finish abstraction for StepTask

This commit is contained in:
anthorob committed 2025-03-09 15:29:43 -04:00
1 parent f01c48c61c
commit 3466fb536d
29 files changed
+725 -133

No files matched your search

@@ -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<String, String> 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" -> {
@@ -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());
@@ -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";
}
}
@@ -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";
}
}
@@ -42,6 +42,7 @@ public class MessageTaskContext extends TaskContext implements MessageContext {
@Override
public void replyEphemeral(MessageCreateData messageCreateData) {
if (getType() == ChannelType.PRIVATE) {
reply(messageCreateData);
return;
}
@@ -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;
@@ -24,6 +24,10 @@ public abstract class AbstractCommand implements Command {
@Override
public DefaultMemberPermissions getSlashCommandPermission() {
if (needAdmin()) {
return DefaultMemberPermissions.DISABLED;
}
return DefaultMemberPermissions.ENABLED;
}
@@ -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() {
@@ -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();
@@ -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;
@@ -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);
}
@@ -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<Task> 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<Task> 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 <E extends Task> Optional<E> findTaskByPartialId(Class<E> clazz, String partialId) {
List<E> tasks = findTasksByPartialId(clazz, partialId);
return Optional.ofNullable(tasks.isEmpty() ? null : tasks.get(0));
}
public <E extends Task> List<E> findTasksByPartialId(Class<E> clazz, String partialId) {
return tasks.values().stream()
.filter(clazz::isInstance)
@@ -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<Task> 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));
}
}
@@ -15,10 +15,12 @@ import java.util.concurrent.ConcurrentHashMap;
public abstract class EmojiTask extends AbstractPersistableTask {
private final Map<String, String> 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<String, String> getEmojiMapping() {
@@ -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<Emoji> 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);
}
@@ -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);
@@ -19,7 +19,11 @@ public interface WithChildren extends Task {
List<Task> getChildTasks();
default void addChild(List<Task> tasks) {
getChildTasks().addAll(tasks);
}
default void addChild(Task... tasks) {
getChildTasks().addAll(List.of(tasks));
addChild(List.of(tasks));
}
}
@@ -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());
@@ -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);
@@ -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<String, String> emojiMapping) {
super(guildId, messageId);
public EmojiReactionRoleTask(String guildId, String channelId, String messageId, Map<String, String> emojiMapping) {
super(guildId, channelId, messageId);
getEmojiMapping().putAll(emojiMapping);
}
@@ -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 <P> the type of the parent task, which must implement WithChildren
*/
public class AcceptButton<P extends WithChildren> extends ButtonTask implements WithParent<P> {
public class AcceptButton<P extends WithChildren> extends ButtonTask implements WithParent<P>, NotAcknowledgeable {
private final P parentTask;
private final Function<TaskContext, Lifecycle> function;
@@ -32,10 +33,18 @@ public class AcceptButton<P extends WithChildren> extends ButtonTask implements
return new AcceptButton<>(parentTask, ButtonStyle.PRIMARY, buttonLabel, function);
}
public static <P extends WithChildren> AcceptButton<P> createSecondaryButton(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new AcceptButton<>(parentTask, ButtonStyle.SECONDARY, buttonLabel, function);
}
public static <P extends WithChildren> AcceptButton<P> createSuccessButton(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new AcceptButton<>(parentTask, ButtonStyle.SUCCESS, buttonLabel, function);
}
public static <P extends WithChildren> AcceptButton<P> createDangerButton(P parentTask, String buttonLabel, Function<TaskContext, Lifecycle> function) {
return new AcceptButton<>(parentTask, ButtonStyle.DANGER, buttonLabel, function);
}
@Override
public P getParentTask() {
return parentTask;
@@ -46,6 +55,11 @@ public class AcceptButton<P extends WithChildren> extends ButtonTask implements
return "accept-" + parentTask.getName();
}
@Override
public boolean isInsertable() {
return true;
}
@Override
public Lifecycle execute(TaskContext context) {
return function.apply(context);
@@ -39,6 +39,11 @@ public class CancelButton<P extends WithChildren> extends ButtonTask implements
return "cancel-" + parentTask.getName();
}
@Override
public boolean isInsertable() {
return true;
}
@Override
public Lifecycle execute(TaskContext context) {
return function.apply(context);
@@ -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<AudioTrack> searchResults = playlist.getTracks().stream().limit(searchResultsLimit).toList();
List<ItemComponent> buttonList = new ArrayList<>();
List<Task> 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));
@@ -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));
}
}
@@ -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<Task> 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<Task> getChildTasks() {
return childrens;
}
}
@@ -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<String> 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";
}
}
@@ -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";
}
}
@@ -18,19 +18,18 @@ public class CleanupTaskThread implements Runnable {
@Override
public void run() {
List<Task> tasks = TaskManager.getInstance().getTasks()
TaskManager manager = TaskManager.getInstance();
List<Task> 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);
}
}
}
@@ -4,10 +4,12 @@
"class": "me.bobert.netneutrality.tasks.emoji.EmojiReactionRoleTask",
"params": [
"guildId",
"channelId",
"messageId",
"emojiMapping"
],
"types": [
"string",
"string",
"string",
"map"