diff --git a/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java b/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java index e16612b..42f2cd7 100644 --- a/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/admin/ReactionCommand.java @@ -12,6 +12,8 @@ import me.bobert.netneutrality.core.command.options.CommandOptionsBuilder; import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; @@ -19,6 +21,8 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; public class ReactionCommand extends AbstractAdminCommand { @@ -74,7 +78,7 @@ public class ReactionCommand extends AbstractAdminCommand { @Override public CommandExecutionResult execute(CommandContext context) { - context.ack(); + context.ephemeral(); String guildId = context.getGuild().getId(); String messageId = context.getOption("messageid", CommandOption::getAsString); @@ -102,25 +106,31 @@ public class ReactionCommand extends AbstractAdminCommand { builder.append("I have found the following tasks for this guild:"); for (EmojiReactionRoleTask item : tasks) { + Map mapping = item.getEmojiMapping().entrySet().stream() + .map(entry -> { + Role role = context.getGuild().getRoleById(entry.getValue()); + return role != null ? Map.entry(entry.getKey(), role.getAsMention()) : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + builder.append("\n- Task ID: ").append(item.getId()).append("\n") - .append(" Message ID: ").append(item.getMessageId()).append("\n") - .append(" Emoji Mapping: ").append(item.getEmojiMapping()).append("\n"); + .append(" Message: ") + .append(String.format(Message.JUMP_URL, item.getGuildId(), item.getChannelId(), item.getMessageId())) + .append("\n") + .append(" Emoji Mappings:\n"); + + if (mapping.isEmpty()) { + builder.append(" No emoji mappings found\n"); + } else { + mapping.forEach((emoji, roleMention) -> + builder.append(" - ").append(emoji).append(" → ").append(roleMention).append("\n") + ); + } } return CommandExecutionResult.success(builder.toString()); } - case "setup" -> { - if (task != null) { - return CommandExecutionResult.error("The message ID is already registered, use the add subcommand instead."); - } - - String emoji = context.getOption("emoji", CommandOption::getAsString); - String role = context.getOption("role", CommandOption::getAsString); - task = new EmojiReactionRoleTask(guildId, messageId, Map.of(emoji, role)); - task.insert(context); - - return CommandExecutionResult.success("Added emoji to message successfully!"); - } case "add" -> { if (task == null) { return CommandExecutionResult.error("The message ID provided isn't registered with the bot"); @@ -129,7 +139,6 @@ public class ReactionCommand extends AbstractAdminCommand { String emoji = context.getOption("emoji", CommandOption::getAsString); String role = context.getOption("role", CommandOption::getAsString); task.addEmoji(emoji, role); - task.save(); return CommandExecutionResult.success("Added emoji to message successfully!"); } case "remove" -> { @@ -140,7 +149,6 @@ public class ReactionCommand extends AbstractAdminCommand { } task.removeEmoji(emoji); - task.save(); return CommandExecutionResult.success("Removed emoji from message successfully!"); } case "delete" -> { diff --git a/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java b/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java index cde88fe..09a65ca 100644 --- a/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/music/QueueCommand.java @@ -14,7 +14,6 @@ import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.utils.MessageFunction; import me.bobert.netneutrality.utils.MusicUtils; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; @@ -26,6 +25,11 @@ public class QueueCommand extends AbstractMusicCommand { return true; } + @Override + public boolean needAdmin() { + return true; + } + @Override public String getCommandName() { return "queue"; @@ -52,11 +56,6 @@ public class QueueCommand extends AbstractMusicCommand { ); } - @Override - public DefaultMemberPermissions getSlashCommandPermission() { - return DefaultMemberPermissions.DISABLED; - } - @Override public CommandExecutionResult execute(CommandContext context) { MusicManager manager = MusicManager.get(context.getGuild()); diff --git a/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java b/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java new file mode 100644 index 0000000..c310c6d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/react/CreateEmojiReaction.java @@ -0,0 +1,44 @@ +package me.bobert.netneutrality.command.react; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.reaction.EmojiRoleFirstStep; +import net.dv8tion.jda.api.entities.Message; + +public class CreateEmojiReaction extends AbstractMessageMenuCommand { + @Override + public CommandExecutionResult execute(Message target, CommandContext context) { + EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null); + + if (task != null) { + context.replyEphemeral("This message is already registered with the bot, please use the modify command instead"); + return CommandExecutionResult.none(); + } + + return CommandExecutionResult.task(new EmojiRoleFirstStep(target)); + } + + @Override + public boolean needAdmin() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getCommandName() { + return "Create emoji role mapping"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java b/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java new file mode 100644 index 0000000..d1d2c9a --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/react/ModifyEmojiReaction.java @@ -0,0 +1,44 @@ +package me.bobert.netneutrality.command.react; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.command.AbstractMessageMenuCommand; +import me.bobert.netneutrality.core.command.CommandExecutionResult; +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.reaction.EmojiRoleModify; +import net.dv8tion.jda.api.entities.Message; + +public class ModifyEmojiReaction extends AbstractMessageMenuCommand { + + @Override + public CommandExecutionResult execute(Message target, CommandContext context) { + EmojiReactionRoleTask task = TaskManager.getInstance().findTaskByPartialId(EmojiReactionRoleTask.class, target.getId()).orElse(null); + if (task == null) { + context.replyEphemeral("This message is not registered with the bot, please use the create command instead"); + return CommandExecutionResult.none(); + } + + return CommandExecutionResult.task(new EmojiRoleModify(target, task)); + } + + @Override + public boolean needAdmin() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getCommandName() { + return "Modify emoji role mapping"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java b/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java index ae17857..9009bec 100644 --- a/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java +++ b/src/main/java/me/bobert/netneutrality/context/tasks/MessageTaskContext.java @@ -42,6 +42,7 @@ public class MessageTaskContext extends TaskContext implements MessageContext { @Override public void replyEphemeral(MessageCreateData messageCreateData) { if (getType() == ChannelType.PRIVATE) { + reply(messageCreateData); return; } 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/command/AbstractCommand.java b/src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java index 8cc01b9..439d57a 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java +++ b/src/main/java/me/bobert/netneutrality/core/command/AbstractCommand.java @@ -24,6 +24,10 @@ public abstract class AbstractCommand implements Command { @Override public DefaultMemberPermissions getSlashCommandPermission() { + if (needAdmin()) { + return DefaultMemberPermissions.DISABLED; + } + return DefaultMemberPermissions.ENABLED; } diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java b/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java index df73543..bc64784 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandExecutionResult.java @@ -15,13 +15,13 @@ public class CommandExecutionResult { private final Result result; private final String message; - private final Object data; + private final Task task; private final Exception exception; - private CommandExecutionResult(Result result, String message, Object data, Exception exception) { + private CommandExecutionResult(Result result, String message, Task task, Exception exception) { this.result = result; this.message = message; - this.data = data; + this.task = task; this.exception = exception; } @@ -33,10 +33,6 @@ public class CommandExecutionResult { this(result, message, null); } - private CommandExecutionResult(Result result, Object data) { - this(result, null, data, null); - } - public Result getResult() { return result; } @@ -45,8 +41,8 @@ public class CommandExecutionResult { return message; } - public Object getData() { - return data; + public Task getTask() { + return task; } public Exception getException() { diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java index 0b506b5..341c60f 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java @@ -98,7 +98,7 @@ public class CommandProcessor { } if (result == TASK) { - TaskManager.getInstance().insert(context, (Task) commandExecutionResult.getData()); + TaskManager.getInstance().insert(context, commandExecutionResult.getTask()); } String message = commandExecutionResult.getMessage(); 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/TaskManager.java b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java index 9de3c61..cd13aa0 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java @@ -9,7 +9,6 @@ import me.bobert.netneutrality.core.LogHandler; import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.InteractionContext; import me.bobert.netneutrality.core.context.TaskContext; -import me.bobert.netneutrality.core.tasks.impl.StepTask; import me.bobert.netneutrality.core.tasks.trait.Deletable; import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; import me.bobert.netneutrality.core.tasks.trait.WithChildren; @@ -81,10 +80,6 @@ public class TaskManager { return tasks.values(); } - public synchronized Collection getPendingTasks() { - return pendingTasks; - } - public void insert(CommandContext context, Task task) { String id = task.getId(); @@ -102,7 +97,7 @@ public class TaskManager { taskService.persistTask(persistableTask); } - if (task instanceof StepTask) { + if (task instanceof IStepTask) { pendingTasks.add(task); } else { tasks.put(id, task); @@ -116,7 +111,7 @@ public class TaskManager { if (task instanceof WithChildren taskWithChildren) { List childrens = taskWithChildren.getChildTasks(); if (childrens == null || childrens.isEmpty()) { - logger.warn("Task [{}]: parent task doesn't create any childs", task.getId()); + logger.warn("Parent task '{}' doesn't create any childs", task.getName()); return; } @@ -126,22 +121,25 @@ public class TaskManager { } } - public synchronized void activateTask(Task task) { + public void activateTask(Task task, String oldId) { if (pendingTasks.remove(task)) { tasks.put(task.getId(), task); - logger.info("Updating task {} with new id: {}", task.getName(), task.getId()); + logger.info("Updating task {} with new id: {} (was: {})", + task.getName(), task.getId(), oldId); } } - public synchronized void removeTask(Task task) { + public void removeTask(Task task) { String id = task.getId(); - if (tasks.containsKey(id)) { - logger.debug("Removing task: {}", task); - tasks.remove(id); - } + if (tasks.remove(id) != null) { + logger.debug("Removing task: {}", id); - tasksLock.remove(task.getLockId()); + ReentrantLock lock = tasksLock.remove(task.getLockId()); + if (lock != null) { + logger.debug("Cleaning up lock for removed task: {}", task.getId()); + } + } } public void handle(TaskContext context) { @@ -166,15 +164,15 @@ public class TaskManager { if (lock != null) { logger.debug("Aquiring lock for task: {}", taskId); lock.lock(); - logger.debug("Aquired lock for: {}", lockId); if (!tasks.containsKey(taskId)) { logger.debug("Task {} is no longer available after aquiring lock", taskId); + tasksLock.remove(lockId); return; } } - logger.debug("Executing task: {}", task); + logger.debug("Executing task: {}", taskId); Task.Lifecycle status = task.execute(context); handleTaskLifecycle(task, status); @@ -201,6 +199,11 @@ public class TaskManager { } } + public Optional findTaskByPartialId(Class clazz, String partialId) { + List tasks = findTasksByPartialId(clazz, partialId); + return Optional.ofNullable(tasks.isEmpty() ? null : tasks.get(0)); + } + public List findTasksByPartialId(Class clazz, String partialId) { return tasks.values().stream() .filter(clazz::isInstance) diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java index fc76810..f18d76e 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java @@ -6,10 +6,14 @@ package me.bobert.netneutrality.core.tasks.impl; */ import me.bobert.netneutrality.core.tasks.GenericRandomIdTask; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; import java.time.OffsetDateTime; +import java.util.List; public abstract class ButtonTask extends GenericRandomIdTask { private final ButtonStyle buttonStyle; @@ -33,4 +37,17 @@ public abstract class ButtonTask extends GenericRandomIdTask { public boolean isExpired() { return getCreationTime().plusMinutes(15).isBefore(OffsetDateTime.now()); } + + public ActionRow createActionRow(List tasks) { + ActionRow row = ActionRow.of(tasks.stream().map(Task::createButton).toList()); + if (this instanceof WithChildren parent) { + parent.addChild(tasks); + } + + return row; + } + + public ActionRow createActionRow(Task... tasks) { + return createActionRow(List.of(tasks)); + } } diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java index 26bc71a..dae4604 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java @@ -15,10 +15,12 @@ import java.util.concurrent.ConcurrentHashMap; public abstract class EmojiTask extends AbstractPersistableTask { private final Map emojiMapping = new ConcurrentHashMap<>(); private final String guildId; + private final String channelId; private final String messageId; - public EmojiTask(String guildId, String messageId) { + public EmojiTask(String guildId, String channelId, String messageId) { this.guildId = guildId; + this.channelId = channelId; this.messageId = messageId; } @@ -26,16 +28,22 @@ public abstract class EmojiTask extends AbstractPersistableTask { return guildId; } + public String getChannelId() { + return channelId; + } + public String getMessageId() { return messageId; } public void addEmoji(String emoji, String snowflake) { emojiMapping.put(emoji, snowflake); + save(); } public void removeEmoji(String emoji) { emojiMapping.remove(emoji); + save(); } public Map getEmojiMapping() { 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); } diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java index 768eb4a..80b89b1 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java @@ -41,7 +41,7 @@ public class TaskFactory { public static PersistableTask createTaskInstance(String className, JSONArray params, JSONArray types, JSONObject payload) throws Exception { if (params.length() != types.length()) { - throw new RuntimeException("Wrong number of parameters and types."); + throw new RuntimeException("Wrong number of parameters and types found in task mappings"); } Class clazz = Class.forName(className); diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java index 22f5fec..22cf018 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java @@ -19,7 +19,11 @@ public interface WithChildren extends Task { List getChildTasks(); + default void addChild(List tasks) { + getChildTasks().addAll(tasks); + } + default void addChild(Task... tasks) { - getChildTasks().addAll(List.of(tasks)); + addChild(List.of(tasks)); } } diff --git a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java index 7e490f9..5685b51 100644 --- a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java +++ b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java @@ -34,7 +34,7 @@ public class TaskService { for (OTask oTask : tasks) { String taskId = oTask.getTaskId(); String taskClass = oTask.getTaskClass(); - logger.debug("Trying to load task with class: {}", taskClass); + logger.debug("Trying to load task with ID: {} and class: {}", taskId, taskClass); String payload = oTask.getPayload(); @@ -95,11 +95,12 @@ public class TaskService { oTask.setPayload(task.toPayload()); + logger.info("Persisting task: {}", oTask.getTaskId()); taskRepository.save(oTask); } public void unpersistTask(PersistableTask task) { - logger.debug("Unpersisting task: {}", task); + logger.debug("Unpersisting task: {}", task.getId()); TaskRepository taskRepository = new TaskRepository(); taskRepository.deleteBy(task.getId()); diff --git a/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java b/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java index 7b3002e..2fe3ade 100644 --- a/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java +++ b/src/main/java/me/bobert/netneutrality/music/TrackLoadingHandler.java @@ -70,7 +70,7 @@ public class TrackLoadingHandler implements AudioLoadResultHandler { @Override public void loadFailed(FriendlyException e) { - logger.debug("Guild {}: load failed for identifier {}", guild.getId(), identifier, e); + logger.debug("Guild {}: load failed for identifier {}", guild.getId(), identifier); //TODO: better error messages RuntimeException exception = new RuntimeException("Failed to load track: " + e.getMessage(), e); diff --git a/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java b/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java index 3591730..a6853eb 100644 --- a/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java +++ b/src/main/java/me/bobert/netneutrality/tasks/emoji/EmojiReactionRoleTask.java @@ -14,12 +14,12 @@ import java.util.Map; public class EmojiReactionRoleTask extends EmojiTask { public EmojiReactionRoleTask(String guildId, String channelId, String messageId) { - super(guildId, messageId); + super(guildId, channelId, messageId); } // constructor for auto loading - public EmojiReactionRoleTask(String guildId, String messageId, Map emojiMapping) { - super(guildId, messageId); + public EmojiReactionRoleTask(String guildId, String channelId, String messageId, Map emojiMapping) { + super(guildId, channelId, messageId); getEmojiMapping().putAll(emojiMapping); } diff --git a/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java b/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java index 2785970..30624e9 100644 --- a/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java +++ b/src/main/java/me/bobert/netneutrality/tasks/generic/AcceptButton.java @@ -7,6 +7,7 @@ package me.bobert.netneutrality.tasks.generic; */ import me.bobert.netneutrality.core.context.TaskContext; +import me.bobert.netneutrality.core.tasks.trait.NotAcknowledgeable; import me.bobert.netneutrality.core.tasks.trait.WithChildren; import me.bobert.netneutrality.core.tasks.trait.WithParent; import me.bobert.netneutrality.core.tasks.impl.ButtonTask; @@ -18,7 +19,7 @@ import java.util.function.Function; * A generic button task that represents a button to accept a choice. * @param

the type of the parent task, which must implement WithChildren */ -public class AcceptButton

extends ButtonTask implements WithParent

{ +public class AcceptButton

extends ButtonTask implements WithParent

, NotAcknowledgeable { private final P parentTask; private final Function function; @@ -32,10 +33,18 @@ public class AcceptButton

extends ButtonTask implements return new AcceptButton<>(parentTask, ButtonStyle.PRIMARY, buttonLabel, function); } + public static

AcceptButton

createSecondaryButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.SECONDARY, buttonLabel, function); + } + public static

AcceptButton

createSuccessButton(P parentTask, String buttonLabel, Function function) { return new AcceptButton<>(parentTask, ButtonStyle.SUCCESS, buttonLabel, function); } + public static

AcceptButton

createDangerButton(P parentTask, String buttonLabel, Function function) { + return new AcceptButton<>(parentTask, ButtonStyle.DANGER, buttonLabel, function); + } + @Override public P getParentTask() { return parentTask; @@ -46,6 +55,11 @@ public class AcceptButton

extends ButtonTask implements return "accept-" + parentTask.getName(); } + @Override + public boolean isInsertable() { + return true; + } + @Override public Lifecycle execute(TaskContext context) { return function.apply(context); diff --git a/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java b/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java index 3747ce8..0d5e8ac 100644 --- a/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java +++ b/src/main/java/me/bobert/netneutrality/tasks/generic/CancelButton.java @@ -39,6 +39,11 @@ public class CancelButton

extends ButtonTask implements return "cancel-" + parentTask.getName(); } + @Override + public boolean isInsertable() { + return true; + } + @Override public Lifecycle execute(TaskContext context) { return function.apply(context); diff --git a/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java b/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java index 319f568..b31eb16 100644 --- a/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java +++ b/src/main/java/me/bobert/netneutrality/tasks/music/LoadPlaylistTask.java @@ -24,7 +24,6 @@ import me.bobert.netneutrality.utils.MusicUtils; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.interactions.components.ActionRow; -import net.dv8tion.jda.api.interactions.components.ItemComponent; import java.util.ArrayList; import java.util.List; @@ -40,10 +39,6 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta this.sourceType = sourceType; } - public AudioPlaylist getPlaylist() { - return playlist; - } - @Override public String getName() { return "load-playlist"; @@ -69,17 +64,14 @@ public class LoadPlaylistTask extends ButtonTask implements WithChildren, Deleta // Search results if (playlist.isSearchResult()) { List searchResults = playlist.getTracks().stream().limit(searchResultsLimit).toList(); - List buttonList = new ArrayList<>(); + List loadTasks = new ArrayList<>(); for (int i = 0; i < searchResults.size(); i++) { AudioTrack track = searchResults.get(i); - Task buttonTask = new LoadTrackFromPlaylistTask(this, String.valueOf(i + 1), track); - - buttonList.add(buttonTask.createButton()); - addChild(buttonTask); + loadTasks.add(new LoadTrackFromPlaylistTask(this, String.valueOf(i + 1), track)); } - ActionRow firstRow = ActionRow.of(buttonList); + ActionRow firstRow = createActionRow(loadTasks); ActionRow secondRow = commonButtons(searchResults, "Add all tracks", "Not for me honey"); context.edit(MessageFunction.buildSearchResultsMessage(searchResults, context.getGuildUser()), List.of(firstRow, secondRow)); diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java new file mode 100644 index 0000000..65c3213 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleFirstStep.java @@ -0,0 +1,47 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; + +public class EmojiRoleFirstStep extends StepTask { + private final Message target; + + public EmojiRoleFirstStep(Message target) { + this.target = target; + } + + @Override + public String getName() { + return "role-first-step"; + } + + @Override + public StepType getStepType() { + return StepType.ROLE; + } + + @Override + public String getStepDescription() { + return String.format("Creating a new emoji role mapping in guild %s for message %s", + getGuild().getId(), + target.getJumpUrl()); + } + + @Override + public String getStepQuestion() { + return "Reply to this message with the role ID you want to configure"; + } + + @Override + public StepResult executeStep(Object answer) { + return StepResult.next(new EmojiRoleSecondStep(target, (Role) answer)); + } + +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java new file mode 100644 index 0000000..40bf58e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleModify.java @@ -0,0 +1,84 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.context.CommandContext; +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.impl.ButtonTask; +import me.bobert.netneutrality.core.tasks.trait.WithChildren; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import me.bobert.netneutrality.tasks.generic.AcceptButton; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.interactions.components.ActionRow; + +import java.util.ArrayList; +import java.util.List; + +public class EmojiRoleModify extends ButtonTask implements WithChildren { + private final List childrens = new ArrayList<>(); + private final Message target; + private final EmojiReactionRoleTask task; + + public EmojiRoleModify(Message target, EmojiReactionRoleTask task) { + super(null, null); + this.target = target; + this.task = task; + } + + @Override + public String getName() { + return "modify emoji role"; + } + + @Override + public boolean isInsertable() { + return false; + } + + @Override + public void replyToInteraction(CommandContext context) { + Task addEmojiTask = AcceptButton.createSuccessButton(this, "Add emoji role mapping", taskContext -> { + if (task.getEmojiMapping().size() >= 10) { + taskContext.replyEphemeral("You cannot add more than 10 emoji roles on a message"); + return Lifecycle.KEEP; + } + + new EmojiRoleFirstStep(target).insert(context); + return Lifecycle.KEEP; + }); + + Task removeEmojiTask = AcceptButton.createSecondaryButton(this, "Remove emoji role mapping", taskContext -> { + if (task.getEmojiMapping().isEmpty()) { + taskContext.replyEphemeral("No emoji role mapping found for the message"); + return Lifecycle.KEEP; + } + + new EmojiRoleRemoveStep(target, task).insert(context); + return Lifecycle.KEEP; + }); + + Task deleteMappingTask = AcceptButton.createDangerButton(this, "Unregister task", taskContext -> { + if (!TaskManager.getInstance().hasTask(task.getId())) { + taskContext.replyEphemeral("This task no longer exists"); + return Lifecycle.REMOVE_SELF; + } + + task.delete(); + context.replyEphemeral("Task deleted successfully!"); + return Lifecycle.REMOVE_SELF; + }); + + ActionRow row = createActionRow(addEmojiTask, removeEmojiTask, deleteMappingTask); + context.replyEphemeral("Select what you want to do with this message", List.of(row)); + } + + @Override + public List getChildTasks() { + return childrens; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java new file mode 100644 index 0000000..0d59a89 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleRemoveStep.java @@ -0,0 +1,62 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; + +import java.util.Set; +import java.util.StringJoiner; + +public class EmojiRoleRemoveStep extends StepTask { + private final Message target; + private final EmojiReactionRoleTask task; + + public EmojiRoleRemoveStep(Message target, EmojiReactionRoleTask task) { + this.target = target; + this.task = task; + } + + @Override + public StepType getStepType() { + return StepType.EMOJI; + } + + @Override + public String getStepDescription() { + return String.format("Select which emoji you want to remove from the message %s", target.getJumpUrl()); + } + + @Override + public String getStepQuestion() { + StringJoiner joiner = new StringJoiner(" - "); + Set emojis = task.getEmojiMapping().keySet(); + for (String emoji : emojis) { + joiner.add(emoji); + } + + return joiner.toString(); + } + + @Override + public StepResult executeStep(Object answer) { + String emoji = (String) answer; + + if (!task.getEmojiMapping().containsKey(emoji)) { + return StepResult.failure("The emoji doesn't exist"); + } + + task.removeEmoji(emoji); + return StepResult.success("Removed emoji successfully!"); + } + + @Override + public String getName() { + return "emoji-role-remove"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java new file mode 100644 index 0000000..4371d13 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/tasks/reaction/EmojiRoleSecondStep.java @@ -0,0 +1,64 @@ +package me.bobert.netneutrality.tasks.reaction; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.core.tasks.Task; +import me.bobert.netneutrality.core.tasks.TaskManager; +import me.bobert.netneutrality.core.tasks.impl.StepTask; +import me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; + +import java.util.Map; + +public class EmojiRoleSecondStep extends StepTask { + private final Message target; + private final Role role; + + public EmojiRoleSecondStep(Message target, Role role) { + this.target = target; + this.role = role; + } + + @Override + public StepType getStepType() { + return StepType.EMOJI; + } + + @Override + public String getStepDescription() { + return String.format("Creating a new emoji role mapping in guild %s for message %s and role %s", + getGuild().getId(), + target.getJumpUrl(), + role.getName()); + } + + @Override + public String getStepQuestion() { + return "Reply with the emoji you want to add to the role"; + } + + @Override + public StepResult executeStep(Object answer) { + String emoji = (String) answer; + Task emojiTask = new EmojiReactionRoleTask(target.getGuildId(), + target.getChannelId(), + target.getId(), + Map.of(emoji, role.getId())); + + if (TaskManager.getInstance().hasTask(emojiTask.getId())) { + return StepResult.failure("The message ID is already registered with the bot, please use the modify mapping command instead"); + } + + return StepResult.success("Mapping registered successfully for role " + role.getName() + " with emoji " + emoji, emojiTask); + } + + @Override + public String getName() { + return "role-second-step"; + } +} diff --git a/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java b/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java index ebf40a0..35931ca 100644 --- a/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java +++ b/src/main/java/me/bobert/netneutrality/thread/CleanupTaskThread.java @@ -18,19 +18,18 @@ public class CleanupTaskThread implements Runnable { @Override public void run() { - List tasks = TaskManager.getInstance().getTasks() + TaskManager manager = TaskManager.getInstance(); + + List expiredTasks = manager.getTasks() .stream() .filter(task -> !(task instanceof PersistableTask)) .filter(task -> !(task instanceof BuiltInTask)) .filter(Task::isExpired) .toList(); - if (!tasks.isEmpty()) { - logger.info("Cleaning up {} expired tasks..", tasks.size()); - - for (Task task : tasks) { - TaskManager.getInstance().removeTask(task); - } + if (!expiredTasks.isEmpty()) { + logger.info("Cleaning up {} expired tasks..", expiredTasks.size()); + expiredTasks.forEach(manager::removeTask); } } } diff --git a/src/main/resources/persistable-tasks.json b/src/main/resources/persistable-tasks.json index 7db2d5a..556499f 100644 --- a/src/main/resources/persistable-tasks.json +++ b/src/main/resources/persistable-tasks.json @@ -4,10 +4,12 @@ "class": "me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask", "params": [ "guildId", + "channelId", "messageId", "emojiMapping" ], "types": [ + "string", "string", "string", "map"