lastArgs = arguments.stream().skip(options.size() - 1).toList();
+ arguments.removeAll(lastArgs);
+ arguments.add(String.join(" ", lastArgs));
+ }
+ }
+
+ if (arguments.size() > required + optional) {
+ context.reply(invalidUsage("too many arguments"));
+ return false;
+ }
+
+ context.setCurrentCommandOptions(arguments.toArray(String[]::new), options);
+ return true;
+ }
+
+ private boolean canExecute(Command command) {
+ if (command.canBeExecutedFrom(context.getChannel())) {
+ return true;
+ }
+
+ if (context instanceof SlashCommandContext) {
+ return true; // no further checks since slash perms are handled by guild settings on discord
+ }
+
+ if (command.canBeExecutedBy(context.getGuildUser())) {
+ return true;
+ }
+
+ return command.needAdmin() && UserFunction.isAdmin(context.getGuildUser());
+ }
+
+ private String invalidUsage(String message) {
+ return String.format("Invalid usage for command: %s\n" +
+ "Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName());
+ }
+
+ private void configureSentry(Command command) {
+ Sentry.configureScope(scope -> {
+ scope.setTag("command", command.getCommandName());
+ scope.setTag("user", context.getGuildUser().getId());
+ scope.setTag("channel", context.getChannel().getId());
+
+ if (context.getGuild() != null) {
+ scope.setTag("guild", context.getGuild().getId());
+ scope.setTag("isAdmin", String.valueOf(UserFunction.isAdmin(context.getGuildUser())));
+ scope.setTag("canBeExecutedBy", String.valueOf(command.canBeExecutedBy(context.getGuildUser())));
+ }
+
+ scope.setTag("contextId", context.getId());
+ });
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java
new file mode 100644
index 0000000..0a17ae2
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOption.java
@@ -0,0 +1,164 @@
+package me.bobert.netneutrality.core.command.options;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+/**
+ * This is a wrapper around {@link OptionData} that can support both slash and message commands
+ */
+public interface CommandOption {
+ OptionType getType();
+
+ /**
+ * The name of this option.
+ *
+ * @return The option name
+ */
+ String getName();
+
+ /**
+ * The file uploaded for this option.
+ *
This is represented as an {@link Message.Attachment#isEphemeral() ephemeral} attachment which will only be hosted for up to 2 weeks.
+ * If you want a permanent reference, you must download it.
+ *
+ * @throws IllegalStateException
+ * If this option {@link #getType() type} is not {@link OptionType#ATTACHMENT}
+ *
+ * @return {@link net.dv8tion.jda.api.entities.Message.Attachment Attachment}
+ */
+ Message.Attachment getAsAttachment();
+
+ /**
+ * The String representation of this option value.
+ *
This will automatically convert the value to a string if the type is not {@link OptionType#STRING OptionType.STRING}.
+ *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
+ *
+ * @return The String representation of this option value
+ */
+ String getAsString();
+
+ /**
+ * The boolean value.
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#BOOLEAN BOOLEAN}
+ *
+ * @return The boolean value
+ */
+ boolean getAsBoolean();
+
+ /**
+ * The long value for this option.
+ *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
+ *
+ * @throws IllegalStateException
+ * If this option {@link #getType() type} cannot be converted to a long
+ * @throws NumberFormatException
+ * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
+ *
+ * @return The long value
+ */
+ long getAsLong();
+
+ /**
+ * The int value for this option.
+ *
This will be the ID of any resolved entity such as {@link Role} or {@link Member}.
+ *
+ * It is highly recommended to assert int values by using {@link OptionData#setRequiredRange(long, long)}
+ *
+ * @throws IllegalStateException
+ * If this option {@link #getType() type} cannot be converted to a long
+ * @throws NumberFormatException
+ * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid long value
+ * @throws ArithmeticException
+ * If the provided integer value cannot fit into a 32bit signed int
+ *
+ * @return The int value
+ */
+ int getAsInt();
+
+ /**
+ * The double value for this option.
+ *
+ * @throws IllegalStateException
+ * If this option {@link #getType() type} cannot be converted to a double
+ * @throws NumberFormatException
+ * If this option is of type {@link OptionType#STRING STRING} and could not be parsed to a valid double value
+ *
+ * @return The double value
+ */
+ double getAsDouble();
+
+ /**
+ * The resolved {@link IMentionable} instance for this option value.
+ *
+ * @throws IllegalStateException
+ * If the mentioned entity is not resolvable
+ *
+ * @return The resolved {@link IMentionable}
+ */
+ IMentionable getAsMentionable();
+
+ /**
+ * The resolved {@link Member} for this option value.
+ *
Note that {@link OptionType#USER OptionType.USER} can also accept users that are not members of a guild, in which case this will be null!
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#USER USER} or {@link OptionType#MENTIONABLE MENTIONABLE}
+ *
+ * @return The resolved {@link Member}, or null
+ */
+ Member getAsMember();
+
+ /**
+ * The resolved {@link User} for this option value.
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#USER USER} or
+ * {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved user
+ *
+ * @return The resolved {@link User}
+ */
+ User getAsUser();
+
+ /**
+ * The resolved {@link Role} for this option value.
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#ROLE ROLE} or
+ * {@link OptionType#MENTIONABLE MENTIONABLE} without a resolved role
+ *
+ * @return The resolved {@link Role}
+ */
+ Role getAsRole();
+
+ /**
+ * The {@link ChannelType} for the resolved channel.
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#CHANNEL CHANNEL}
+ *
+ * @return The {@link ChannelType}
+ */
+ ChannelType getChannelType();
+
+ /**
+ * The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel} for this option value.
+ *
Note that {@link OptionType#CHANNEL OptionType.CHANNEL} can accept channels of any type!
+ *
+ * @throws IllegalStateException
+ * If this option is not of type {@link OptionType#CHANNEL CHANNEL}
+ * or could not be resolved for unexpected reasons
+ *
+ * @return The resolved {@link net.dv8tion.jda.api.entities.channel.middleman.GuildChannel}
+ */
+ GuildChannelUnion getAsChannel();
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java
new file mode 100644
index 0000000..70d55cd
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptions.java
@@ -0,0 +1,170 @@
+package me.bobert.netneutrality.core.command.options;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.command.Command;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
+import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
+import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
+import net.dv8tion.jda.internal.interactions.CommandDataImpl;
+
+import java.util.List;
+import java.util.Set;
+import java.util.StringJoiner;
+
+public class CommandOptions {
+ private final Command command;
+ private final CommandDataImpl commandData;
+ private final List options;
+ private final List subcommands;
+ private final List subcommandGroups;
+
+ public CommandOptions(Command command, CommandDataImpl commandData) {
+ this.command = command;
+ this.commandData = commandData;
+ this.options = commandData.getOptions();
+ this.subcommands = commandData.getSubcommands();
+ this.subcommandGroups = commandData.getSubcommandGroups();
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+
+ public SlashCommandData getSlashCommandData() {
+ return commandData;
+ }
+
+ public List getOptions() {
+ return options;
+ }
+
+ public List getSubcommands() {
+ return subcommands;
+ }
+
+ public List getSubcommandGroups() {
+ return subcommandGroups;
+ }
+
+ public boolean isEmpty() {
+ return options.isEmpty() && subcommands.isEmpty() && subcommandGroups.isEmpty();
+ }
+
+ public String printUsage() {
+ StringBuilder usage = new StringBuilder();
+ if (!options.isEmpty()) {
+ usage.append("- Usage: /").append(command.getCommandName());
+ for (OptionData option : options) {
+ usage.append(" <").append(option.getName()).append(">");
+ }
+
+ usage.append(printOptions(options, null, false));
+ }
+
+ if (!subcommands.isEmpty()) {
+ for (SubcommandData subcommand : subcommands) {
+ usage.append("- Usage: /").append(command.getCommandName()).append(" ").append(subcommand.getName());
+
+ for (OptionData option : subcommand.getOptions()) {
+ usage.append(" <").append(option.getName()).append(">");
+ }
+
+ usage.append(" (").append(subcommand.getDescription()).append(")");
+ usage.append(printOptions(subcommand.getOptions(), null, false));
+
+ usage.append("\n");
+ }
+ }
+
+ if (!subcommandGroups.isEmpty()) {
+ for (SubcommandGroupData subcommandGroup : subcommandGroups) {
+ usage.append("- Subcommand: /").append(command.getCommandName())
+ .append(" ").append(subcommandGroup.getName())
+ .append(" (").append(subcommandGroup.getDescription()).append(")")
+ .append("\n");
+
+ for (SubcommandData subcommand : subcommandGroup.getSubcommands()) {
+ usage.append(" - Usage: /").append(command.getCommandName())
+ .append(" ").append(subcommandGroup.getName())
+ .append(" ").append(subcommand.getName());
+
+ for (OptionData option : subcommand.getOptions()) {
+ usage.append(" <").append(option.getName()).append(">");
+ }
+
+ usage.append(printOptions(subcommand.getOptions(), subcommand.getDescription(), true));
+
+ usage.append("\n");
+ }
+ }
+ }
+
+ if (!isEmpty()) {
+ usage.append("\n\n-# *: required argument");
+ }
+
+ return usage.toString();
+ }
+
+ private String printOptions(List options, String description, boolean group) {
+ StringBuilder usage = new StringBuilder();
+
+ if (description != null) {
+ usage.append(" (").append(description).append(")");
+ }
+
+ for (OptionData option : options) {
+ usage.append("\n");
+ if (group) usage.append(" ");
+ usage.append(" - ");
+ if (option.isRequired()) {
+ usage.append("*");
+ }
+ usage.append(option.getName()).append(": ").append(option.getDescription());
+
+ if (option.getMinValue() != null) {
+ usage.append(" [Min value: ").append(option.getMinValue().longValue()).append("]");
+ }
+
+ if (option.getMaxValue() != null) {
+ usage.append(" [Max value: ").append(option.getMaxValue().longValue()).append("]");
+ }
+
+ if (option.getMinLength() != null) {
+ usage.append(" [Min length: ").append(option.getMinLength().longValue()).append("]");
+ }
+
+ if (option.getMaxLength() != null) {
+ usage.append(" [Max length: ").append(option.getMaxLength().longValue()).append("]");
+ }
+
+ List choices = option.getChoices();
+ if (!choices.isEmpty()) {
+ StringJoiner joiner = new StringJoiner(", ");
+ for (net.dv8tion.jda.api.interactions.commands.Command.Choice choice : choices) {
+ joiner.add(choice.getAsString());
+ }
+
+ usage.append(" [Available choices: ").append(joiner).append("]");
+ }
+
+ Set requiredChannelTypes = option.getChannelTypes();
+ if (!requiredChannelTypes.isEmpty()) {
+ StringJoiner joiner = new StringJoiner(", ");
+ for (ChannelType type : requiredChannelTypes) {
+ joiner.add(type.name());
+ }
+
+ usage.append(" [Required channel types: ").append(joiner).append("]");
+ }
+ }
+
+ return usage.toString();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java
new file mode 100644
index 0000000..20652d4
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/command/options/CommandOptionsBuilder.java
@@ -0,0 +1,185 @@
+package me.bobert.netneutrality.core.command.options;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.command.Command;
+import net.dv8tion.jda.api.interactions.DiscordLocale;
+import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
+import net.dv8tion.jda.api.interactions.commands.build.*;
+import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction;
+import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap;
+import net.dv8tion.jda.api.utils.data.DataObject;
+import net.dv8tion.jda.internal.interactions.CommandDataImpl;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class CommandOptionsBuilder implements SlashCommandData {
+ private final Command command;
+ private CommandDataImpl commandData;
+
+ public CommandOptionsBuilder(Command.Type type, Command command) {
+ this.command = command;
+
+ if (type == Command.Type.SLASH) {
+ commandData = (CommandDataImpl) Commands.slash(command.getCommandName(), command.getDescription());
+ }
+
+ if (type == Command.Type.USER) {
+ commandData = (CommandDataImpl) Commands.user(command.getCommandName());
+ }
+
+ if (type == Command.Type.MESSAGE) {
+ commandData = (CommandDataImpl) Commands.message(command.getCommandName());
+ }
+ }
+
+ public CommandOptions build() {
+ return new CommandOptions(command, commandData);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setLocalizationFunction(@Nonnull LocalizationFunction localizationFunction) {
+ return commandData.setLocalizationFunction(localizationFunction);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setName(@Nonnull String name) {
+ return commandData.setName(name);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setNameLocalization(@Nonnull DiscordLocale locale, @Nonnull String name) {
+ return commandData.setNameLocalization(locale, name);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setNameLocalizations(@Nonnull Map map) {
+ return commandData.setNameLocalizations(map);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setDefaultPermissions(@Nonnull DefaultMemberPermissions permission) {
+ return commandData.setDefaultPermissions(permission);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setGuildOnly(boolean guildOnly) {
+ return commandData.setGuildOnly(guildOnly);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setNSFW(boolean nsfw) {
+ return commandData.setNSFW(nsfw);
+ }
+
+ @Override
+ public @Nonnull String getName() {
+ return commandData.getName();
+ }
+
+ @Override
+ public @Nonnull LocalizationMap getNameLocalizations() {
+ return commandData.getNameLocalizations();
+ }
+
+ @Override
+ public @Nonnull net.dv8tion.jda.api.interactions.commands.Command.Type getType() {
+ return commandData.getType();
+ }
+
+ @Override
+ public @Nonnull DefaultMemberPermissions getDefaultPermissions() {
+ return commandData.getDefaultPermissions();
+ }
+
+ @Override
+ public boolean isGuildOnly() {
+ return commandData.isGuildOnly();
+ }
+
+ @Override
+ public boolean isNSFW() {
+ return commandData.isNSFW();
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setDescription(@Nonnull String description) {
+ return commandData.setDescription(description);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setDescriptionLocalization(@Nonnull DiscordLocale locale, @Nonnull String description) {
+ return commandData.setDescriptionLocalization(locale, description);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData setDescriptionLocalizations(@Nonnull Map map) {
+ return commandData.setDescriptionLocalizations(map);
+ }
+
+ @Override
+ public @Nonnull String getDescription() {
+ return commandData.getDescription();
+ }
+
+ @Override
+ public @Nonnull LocalizationMap getDescriptionLocalizations() {
+ return commandData.getDescriptionLocalizations();
+ }
+
+ @Override
+ public boolean removeOptions(@Nonnull Predicate super OptionData> condition) {
+ return commandData.removeOptions(condition);
+ }
+
+ @Override
+ public boolean removeSubcommands(@Nonnull Predicate super SubcommandData> condition) {
+ return commandData.removeSubcommands(condition);
+ }
+
+ @Override
+ public boolean removeSubcommandGroups(@Nonnull Predicate super SubcommandGroupData> condition) {
+ return commandData.removeSubcommandGroups(condition);
+ }
+
+ @Override
+ public @Nonnull List getSubcommands() {
+ return commandData.getSubcommands();
+ }
+
+ @Override
+ public @Nonnull List getSubcommandGroups() {
+ return commandData.getSubcommandGroups();
+ }
+
+ @Override
+ public @Nonnull List getOptions() {
+ return commandData.getOptions();
+ }
+
+ @Override
+ public @Nonnull SlashCommandData addOptions(@Nonnull OptionData... options) {
+ return commandData.addOptions(options);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData addSubcommands(@Nonnull SubcommandData... subcommands) {
+ return commandData.addSubcommands(subcommands);
+ }
+
+ @Override
+ public @Nonnull SlashCommandData addSubcommandGroups(@Nonnull SubcommandGroupData... groups) {
+ return commandData.addSubcommandGroups(groups);
+ }
+
+ @Override
+ public @Nonnull DataObject toData() {
+ return commandData.toData();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java
new file mode 100644
index 0000000..ab1f0b3
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/command/options/MessageCommandOption.java
@@ -0,0 +1,157 @@
+package me.bobert.netneutrality.core.command.options;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+
+public class MessageCommandOption implements CommandOption {
+ private final OptionData data;
+ private final OptionType type;
+ private final Object value;
+
+ public MessageCommandOption(OptionData data, Object value) {
+ this.data = data;
+ this.type = data.getType();
+ this.value = value;
+ }
+
+ @Override
+ public OptionType getType() {
+ return data.getType();
+ }
+
+ @Override
+ public String getName() {
+ return data.getName();
+ }
+
+ @Override
+ public Message.Attachment getAsAttachment() {
+ throw new UnsupportedOperationException("Not supported for message commands");
+ }
+
+ @Override
+ public String getAsString() {
+ return switch (type) {
+ case STRING -> (String) value;
+ case BOOLEAN -> String.valueOf((boolean) value);
+ case NUMBER -> String.valueOf((double) value);
+ case MENTIONABLE -> ((IMentionable) value).getId();
+ case CHANNEL -> ((GuildChannelUnion) value).getId();
+ case ROLE -> ((Role) value).getId();
+ case USER -> ((User) value).getId();
+ case INTEGER -> String.valueOf((long) value);
+ case ATTACHMENT -> ((Message.Attachment) value).getId();
+ default -> throw new IllegalStateException("Cannot convert option of type " + type + " to long");
+ };
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ if (type != OptionType.BOOLEAN) {
+ throw new IllegalStateException("Cannot convert option of type " + type + " to boolean");
+ }
+
+ return (boolean) value;
+ }
+
+ @Override
+ public long getAsLong() {
+ return switch (type) {
+ case STRING -> Long.parseLong((String) value);
+ case MENTIONABLE -> ((IMentionable) value).getIdLong();
+ case CHANNEL -> ((GuildChannelUnion) value).getIdLong();
+ case ROLE -> ((Role) value).getIdLong();
+ case USER -> ((User) value).getIdLong();
+ case INTEGER -> (long) value;
+ case NUMBER -> ((Double) value).longValue();
+ case ATTACHMENT -> ((Message.Attachment) value).getIdLong();
+ default -> throw new IllegalStateException("Cannot convert option of type " + type + " to long");
+ };
+ }
+
+ @Override
+ public int getAsInt() {
+ return Math.toIntExact(getAsLong());
+ }
+
+ @Override
+ public double getAsDouble() {
+ return switch (type) {
+ case STRING -> Double.parseDouble((String) value);
+ case NUMBER -> (double) value;
+ case INTEGER -> (int) value;
+ default -> throw new IllegalStateException("Cannot convert option of type " + type + " to double");
+ };
+ }
+
+ @Override
+ public IMentionable getAsMentionable() {
+ if (value instanceof IMentionable) {
+ return (IMentionable) value;
+ }
+
+ throw new IllegalStateException("Cannot resolve option of type " + type + " to IMentionable");
+ }
+
+ @Override
+ public Member getAsMember() {
+ if (value instanceof Member) {
+ return (Member) value;
+ }
+
+ if (value instanceof User) {
+ return null;
+ }
+
+ throw new IllegalStateException("Cannot resolve option of type " + type + " to Member");
+ }
+
+ @Override
+ public User getAsUser() {
+ if (value instanceof User) {
+ return (User) value;
+ }
+
+ throw new IllegalStateException("Cannot resolve option of type " + type + " to User");
+ }
+
+ @Override
+ public Role getAsRole() {
+ if (value instanceof Role) {
+ return (Role) value;
+ }
+
+ throw new IllegalStateException("Cannot resolve option of type " + type + " to Role");
+ }
+
+ @Override
+ public ChannelType getChannelType() {
+ return getAsChannel().getType();
+ }
+
+ @Override
+ public GuildChannelUnion getAsChannel() {
+ if (value instanceof GuildChannelUnion) {
+ return (GuildChannelUnion) value;
+ }
+
+ throw new IllegalStateException("Cannot resolve option of type " + type + " to GuildChannelUnion");
+ }
+
+ @Override
+ public String toString() {
+ return "MessageCommandOption{" +
+ "name=" + data.getName() +
+ ", type=" + type +
+ ", value=" + value +
+ '}';
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java b/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java
new file mode 100644
index 0000000..90a8c0d
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/command/options/SlashCommandOption.java
@@ -0,0 +1,90 @@
+package me.bobert.netneutrality.core.command.options;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
+import net.dv8tion.jda.api.interactions.commands.OptionMapping;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+
+public class SlashCommandOption implements CommandOption {
+ private final OptionMapping mapping;
+
+ public SlashCommandOption(OptionMapping mapping) {
+ this.mapping = mapping;
+ }
+
+ @Override
+ public OptionType getType() {
+ return mapping.getType();
+ }
+
+ @Override
+ public String getName() {
+ return mapping.getName();
+ }
+
+ @Override
+ public Message.Attachment getAsAttachment() {
+ return mapping.getAsAttachment();
+ }
+
+ @Override
+ public String getAsString() {
+ return mapping.getAsString();
+ }
+
+ @Override
+ public boolean getAsBoolean() {
+ return mapping.getAsBoolean();
+ }
+
+ @Override
+ public long getAsLong() {
+ return mapping.getAsLong();
+ }
+
+ @Override
+ public int getAsInt() {
+ return mapping.getAsInt();
+ }
+
+ @Override
+ public double getAsDouble() {
+ return mapping.getAsDouble();
+ }
+
+ @Override
+ public IMentionable getAsMentionable() {
+ return mapping.getAsMentionable();
+ }
+
+ @Override
+ public Member getAsMember() {
+ return mapping.getAsMember();
+ }
+
+ @Override
+ public User getAsUser() {
+ return mapping.getAsUser();
+ }
+
+ @Override
+ public Role getAsRole() {
+ return mapping.getAsRole();
+ }
+
+ @Override
+ public ChannelType getChannelType() {
+ return mapping.getChannelType();
+ }
+
+ @Override
+ public GuildChannelUnion getAsChannel() {
+ return mapping.getAsChannel();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java b/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java
new file mode 100644
index 0000000..ca55682
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/AbstractContext.java
@@ -0,0 +1,62 @@
+package me.bobert.netneutrality.core.context;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import io.sentry.Sentry;
+import me.bobert.netneutrality.context.command.SlashCommandContext;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+
+public abstract class AbstractContext implements Context {
+ private final JDA jda;
+ private final Guild guild;
+ private final MessageChannel channel;
+ private final Member member;
+ private final User user;
+
+ public AbstractContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
+ this.jda = jda;
+ this.guild = guild;
+ this.channel = channel;
+ this.member = member;
+ this.user = user;
+
+ if (user != null) {
+ io.sentry.protocol.User sentryUser = new io.sentry.protocol.User();
+ sentryUser.setId(user.getId());
+ sentryUser.setUsername(user.getEffectiveName());
+ Sentry.setUser(sentryUser);
+ }
+ }
+
+ @Override
+ public JDA getJDA() {
+ return jda;
+ }
+
+ @Override
+ public Guild getGuild() {
+ return guild;
+ }
+
+ @Override
+ public MessageChannel getChannel() {
+ return channel;
+ }
+
+ @Override
+ public Member getGuildUser() {
+ return member;
+ }
+
+ @Override
+ public User getDiscordUser() {
+ return user;
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java
index 74d7abb..a950e28 100644
--- a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java
+++ b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java
@@ -1,60 +1,138 @@
package me.bobert.netneutrality.core.context;
-
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
-import me.bobert.netneutrality.command.AbstractCommand;
+import me.bobert.netneutrality.core.command.Command;
+import me.bobert.netneutrality.core.command.options.CommandOption;
+import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
-import net.dv8tion.jda.api.events.Event;
-import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
+import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
-public interface CommandContext {
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
- AbstractCommand getCommand();
+public abstract class CommandContext extends AbstractContext {
+ private Command command;
+ private SubcommandGroupData subcommandGroup;
+ private SubcommandData subcommand;
+ private final Map invalidOptions = new HashMap<>();
+ private final List options;
- Event getEvent();
-
- Guild getGuild();
-
- MessageChannel getChannel();
-
- Member getGuildUser();
-
- User getDiscordUser();
-
- JDA getJDA();
-
- void deleteMessage();
-
- void reply(MessageCreateData message);
-
- void reply(String message);
-
- void reply(MessageEmbed... embeds);
-
- default void reply(String format, Object... arguments) {
- reply(String.format(format, arguments));
+ public CommandContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
+ super(jda, guild, channel, member, user);
+ this.options = new ArrayList<>();
}
- void replyToMessage(String message);
-
- default void replyToMessage(MessageCreateData message) {
- reply(message);
+ public Command getCommand() {
+ return command;
}
- default void replyToMessage(String format, Object... arguments) {
- replyToMessage(String.format(format, arguments));
+ public void setCommand(Command command) {
+ this.command = command;
}
- default void replyToMessage(MessageEmbed... embeds) {
- replyToMessage(MessageCreateData.fromEmbeds(embeds));
+ public void setSubcommandGroup(SubcommandGroupData subcommandGroup) {
+ this.subcommandGroup = subcommandGroup;
}
+
+ public void setSubcommand(SubcommandData subcommand) {
+ this.subcommand = subcommand;
+ }
+
+ public void setCurrentCommandOptions(String[] args, List options) {
+ this.options.clear();
+ this.options.addAll(parseOptions(args, options));
+ }
+
+ protected void invalidateContext(OptionData option, String message) {
+ invalidOptions.put(option, message);
+ }
+
+ public boolean isValid() {
+ return invalidOptions.isEmpty();
+ }
+
+ public Set> getInvalidOptions() {
+ return invalidOptions.entrySet();
+ }
+
+ public T getOption(String name, Supplier extends T> fallback, Function super CommandOption, ? extends T> resolver) {
+ Objects.requireNonNull(resolver, "resolver cannot be null");
+ CommandOption mapping = getOption(name);
+ if (mapping != null)
+ return resolver.apply(mapping);
+ return fallback == null ? null : fallback.get();
+ }
+
+ public T getOption(String name, T fallback, Function super CommandOption, ? extends T> resolver) {
+ Objects.requireNonNull(resolver, "resolver cannot be null");
+ CommandOption mapping = getOption(name);
+ if (mapping != null)
+ return resolver.apply(mapping);
+ return fallback;
+ }
+
+ public T getOption(String name, Function super CommandOption, ? extends T> resolver) {
+ return getOption(name, null, resolver);
+ }
+
+ public CommandOption getOption(String name) {
+ List options = getOptionsByName(name);
+ return options.isEmpty() ? null : options.get(0);
+ }
+
+ public List getOptionsByName(String name) {
+ Objects.requireNonNull(name, "name cannot be null");
+ return options.stream().filter(opt -> opt.getName().equals(name)).collect(Collectors.toList());
+ }
+
+ public String getSubcommandGroup() {
+ if (subcommandGroup == null) {
+ return null;
+ }
+
+ return subcommandGroup.getName();
+ }
+
+ public String getSubcommand() {
+ if (subcommand == null) {
+ return null;
+ }
+
+ return subcommand.getName();
+ }
+
+ public boolean isMessageFromOwner() {
+ if (getGuildUser() == null) {
+ return false;
+ }
+
+ return getGuildUser().isOwner();
+ }
+
+ public boolean isMessageFromCreator() {
+ String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER");
+
+ return creatorId.equals(getDiscordUser().getId());
+ }
+
+ public abstract List parseOptions(String[] args, List commandOptions);
+
+ public abstract String parseCommandName();
+
+ public abstract String getFullCommand();
+
+
+
}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/Context.java b/src/main/java/me/bobert/netneutrality/core/context/Context.java
new file mode 100644
index 0000000..bcf7ebd
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/Context.java
@@ -0,0 +1,208 @@
+package me.bobert.netneutrality.core.context;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.AsyncContext;
+import me.bobert.netneutrality.utils.ContextUtils;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+import net.dv8tion.jda.api.events.Event;
+import net.dv8tion.jda.api.interactions.InteractionHook;
+import net.dv8tion.jda.api.interactions.components.LayoutComponent;
+import net.dv8tion.jda.api.interactions.modals.Modal;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+import net.dv8tion.jda.api.utils.messages.MessageEditData;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface Context {
+ Event getEvent();
+
+ String getId();
+
+ ChannelType getType();
+
+ JDA getJDA();
+
+ InteractionHook getInteractionHook();
+
+ Guild getGuild();
+
+ MessageChannel getChannel();
+
+ Member getGuildUser();
+
+ User getDiscordUser();
+
+ boolean isAcknowledged();
+
+ boolean isFromGuild();
+
+ void deleteMessage();
+
+ void reply(Modal modal);
+
+ void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer);
+
+ void defer(boolean ephemeral, Consumer consumer);
+
+ void ack(boolean ephemeral);
+
+ default void ack() {
+ ack(false);
+ }
+
+ default void ephemeral() {
+ ack(true);
+ }
+
+ default void reply(MessageCreateData messageCreateData) {
+ reply(messageCreateData, false, List.of(), asyncMessage -> {});
+ }
+
+ default void reply(MessageCreateData messageCreateData, List components) {
+ reply(messageCreateData, false, components, asyncMessage -> {});
+ }
+
+ default void reply(MessageCreateData messageCreateData, Consumer consumer) {
+ reply(messageCreateData, false, List.of(), consumer);
+ }
+
+ default void reply(String message) {
+ replyWithLimit(message);
+ }
+
+ default void reply(String message, Consumer consumer) {
+ reply(MessageCreateData.fromContent(message), consumer);
+ }
+
+ default void reply(MessageEmbed embed) {
+ reply(List.of(embed));
+ }
+
+ default void reply(List embeds) {
+ reply(MessageCreateData.fromEmbeds(embeds));
+ }
+
+ default void edit(MessageEditData messageEditData, boolean ephemeral, List components, Consumer consumer) {
+ if (isAcknowledged() && getInteractionHook() != null) {
+ getInteractionHook().editOriginal(messageEditData).setComponents(components).queue(message1 -> consumer.accept(new AsyncContext(message1)));
+ return;
+ }
+
+ reply(MessageCreateData.fromEditData(messageEditData), ephemeral, components, consumer);
+ }
+
+ default void edit(MessageEditData messageEditData, List components) {
+ edit(messageEditData, false, components, asyncMessage -> {});
+ }
+
+ default void edit(MessageEditData messageEditData) {
+ edit(messageEditData, false, List.of(), asyncMessage -> {});
+ }
+
+ default void edit(String message) {
+ edit(MessageEditData.fromContent(message));
+ }
+
+ default void edit(MessageEmbed embed) {
+ edit(List.of(embed), List.of());
+ }
+
+ default void edit(List embeds, List components) {
+ edit(MessageEditData.fromEmbeds(embeds), components);
+ }
+
+ default void edit(MessageEmbed embed, List components) {
+ edit(List.of(embed), components);
+ }
+
+ default void async(MessageCreateData messageCreateData, Consumer consumer) {
+ reply(messageCreateData, false, List.of(), consumer);
+ }
+
+ default void async(String message, Consumer consumer) {
+ async(MessageCreateData.fromContent(message), consumer);
+ }
+
+ default void async(Consumer consumer) {
+ defer(false, consumer);
+ }
+
+ default void replyEphemeral(MessageCreateData messageCreateData, List components) {
+ reply(messageCreateData, true, components, asyncMessage -> {});
+ }
+
+ default void replyEphemeral(MessageCreateData messageCreateData) {
+ replyEphemeral(messageCreateData, List.of());
+ }
+
+ default void replyEphemeral(String message) {
+ replyEphemeralWithLimit(message);
+ }
+
+ default void replyEphemeral(String message, List components) {
+ replyEphemeral(MessageCreateData.fromContent(message), components);
+ }
+
+ default void replyEphemeral(List embeds) {
+ replyEphemeral(MessageCreateData.fromEmbeds(embeds));
+ }
+
+ default void sendMessageInPrivate(MessageCreateData messageCreateData, List components, Consumer consumer) {
+ User user = getDiscordUser();
+ if (user != null) {
+ user.openPrivateChannel()
+ .flatMap(channel -> channel.sendMessage(messageCreateData).setComponents(components))
+ .queue(message -> consumer.accept(new AsyncContext(message)));
+ }
+ }
+
+ default void sendMessageInPrivate(String message, List components, Consumer consumer) {
+ sendMessageInPrivate(MessageCreateData.fromContent(message), components, consumer);
+ }
+
+ default void sendMessageInPrivate(MessageCreateData messageCreateData, List components) {
+ sendMessageInPrivate(messageCreateData, components, asyncContext -> {});
+ }
+
+ default void sendMessageInPrivate(String message, Consumer consumer) {
+ sendMessageInPrivate(message, List.of(), consumer);
+ }
+
+ default void sendMessageInPrivate(MessageCreateData messageCreateData) {
+ sendMessageInPrivate(messageCreateData, List.of());
+ }
+
+ default void sendMessageInPrivate(List embeds) {
+ sendMessageInPrivate(MessageCreateData.fromEmbeds(embeds));
+ }
+
+ default void sendMessageInPrivate(String message) {
+ sendMessageInPrivateWithLimit(message);
+ }
+
+ private void replyWithLimit(String message) {
+ for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) {
+ reply(MessageCreateData.fromContent(chunk));
+ }
+ }
+
+ private void replyEphemeralWithLimit(String message) {
+ for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) {
+ replyEphemeral(MessageCreateData.fromContent(chunk));
+ }
+ }
+
+ private void sendMessageInPrivateWithLimit(String message) {
+ for (String chunk : ContextUtils.splitMessage(message, Message.MAX_CONTENT_LENGTH)) {
+ sendMessageInPrivate(MessageCreateData.fromContent(chunk));
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java b/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java
new file mode 100644
index 0000000..9008e75
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/InteractionContext.java
@@ -0,0 +1,92 @@
+package me.bobert.netneutrality.core.context;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.AsyncContext;
+import net.dv8tion.jda.api.interactions.Interaction;
+import net.dv8tion.jda.api.interactions.InteractionHook;
+import net.dv8tion.jda.api.interactions.callbacks.IModalCallback;
+import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
+import net.dv8tion.jda.api.interactions.components.LayoutComponent;
+import net.dv8tion.jda.api.interactions.modals.Modal;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface InteractionContext extends Context {
+
+ @Override
+ default String getId() {
+ return getInteraction().getId();
+ }
+
+ @Override
+ default InteractionHook getInteractionHook() {
+ if (!isAcknowledged()) {
+ throw new RuntimeException("Interaction isn't acknowledged yet");
+ }
+
+ return getReplyCallback().getHook();
+ }
+
+ @Override
+ default void ack(boolean ephemeral) {
+ defer(ephemeral, asyncMessage -> {});
+ }
+
+ @Override
+ default void defer(boolean ephemeral, Consumer consumer) {
+ if (isAcknowledged()) {
+ throw new RuntimeException("Interaction is already acknowledged, please use getInteractionHook() instead.");
+ }
+
+ getReplyCallback().deferReply(ephemeral).queue(hook -> consumer.accept(new AsyncContext(hook)));
+ }
+
+ @Override
+ default boolean isFromGuild() {
+ return getInteraction().isFromGuild();
+ }
+
+ @Override
+ default boolean isAcknowledged() {
+ return getInteraction().isAcknowledged();
+ }
+
+ @Override
+ default void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) {
+ if (isAcknowledged()) {
+ getInteractionHook().sendMessage(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
+ return;
+ }
+
+ getReplyCallback().reply(messageCreateData).setEphemeral(ephemeral).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
+ }
+
+ @Override
+ default void reply(Modal modal) {
+ if (getInteraction() instanceof IModalCallback) {
+ ((IModalCallback) getInteraction()).replyModal(modal).queue();
+ }
+ }
+
+ private IReplyCallback getReplyCallback() {
+ try {
+ return (IReplyCallback) getEvent();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(getEvent().getClass().getName() + " does not implement class 'IReplyCallback'", e);
+ }
+ }
+
+ private Interaction getInteraction() {
+ try {
+ return (Interaction) getEvent();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(getEvent().getClass().getName() + " does not implement class 'Interaction'", e);
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java
deleted file mode 100644
index 9ef63af..0000000
--- a/src/main/java/me/bobert/netneutrality/core/context/MessageCommandContext.java
+++ /dev/null
@@ -1,192 +0,0 @@
-package me.bobert.netneutrality.core.context;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import io.sentry.Breadcrumb;
-import io.sentry.Sentry;
-import me.bobert.netneutrality.command.AbstractCommand;
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.thread.ThreadManager;
-import me.bobert.netneutrality.utils.Settings;
-import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.*;
-import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
-import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
-import net.dv8tion.jda.api.utils.messages.MessageCreateData;
-
-import java.util.Collection;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-
-public class MessageCommandContext implements CommandContext {
-
- private static final ExecutorService commandExecutor = ThreadManager.getExecutor();
-
- private AbstractCommand command;
- private final MessageReceivedEvent event;
- private final Message message;
- private final Guild guild;
- private final MessageChannel channel;
- private final Member member;
- private final User user;
- private final JDA jda;
-
- public MessageCommandContext(MessageReceivedEvent event, Message message, Member member, User user, Guild guild, MessageChannel channel, JDA jda) {
- this.event = event;
- this.message = message;
- this.member = member;
- this.guild = guild;
- this.channel = channel;
- this.user = user;
- this.jda = jda;
-
- io.sentry.protocol.User sentryUser = new io.sentry.protocol.User();
- sentryUser.setId(member.getId());
- sentryUser.setUsername(member.getEffectiveName());
- Sentry.setUser(sentryUser);
- Sentry.addBreadcrumb(Breadcrumb.info("New context for message id " + message.getId()));
- }
-
- public static MessageCommandContext createContext(MessageReceivedEvent event) {
- Message message = event.getMessage();
- Member member = event.getMember();
- User user = event.getAuthor();
- Guild guild = event.getGuild();
- MessageChannel channel = event.getChannel();
- JDA jda = event.getJDA();
-
- return new MessageCommandContext(event, message, member, user, guild, channel, jda);
- }
-
- @Override
- public MessageReceivedEvent getEvent() {
- return event;
- }
-
- public String getArgument(int index) {
- return getArgs()[index];
- }
-
- public String[] getArgs() {
- String msg = message.getContentDisplay().trim();
- msg = msg.substring(Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$").length()).trim();
- String cmd = msg.split(" ", 2)[0];
- String[] args = msg.replaceFirst("(" + cmd + ")(\\s+)?", "").split("\s");
-
- if (args.length == 1 && args[0].isEmpty())
- args = new String[0];
-
- return args;
- }
-
- public String getMessageText() {
- String msg = message.getContentDisplay().trim();
- msg = msg.substring(Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$").length()).trim();
- String cmd = msg.split(" ", 2)[0];
-
- return msg.replaceFirst("(" + cmd + ")(\s)?", "");
- }
-
- public Message getMessage() {
- return message;
- }
-
- public Guild getGuild() {
- return guild;
- }
-
- public MessageChannel getChannel() {
- return channel;
- }
-
- public Member getGuildUser() {
- return member;
- }
-
- public User getDiscordUser() {
- return user;
- }
-
- public JDA getJDA() {
- return jda;
- }
-
- public AbstractCommand getCommand() {
- return command;
- }
-
- public GuildManager getGuildManager() {
- return GuildManager.getManager(guild);
- }
-
- public void reply(String message) {
- reply(MessageCreateData.fromContent(message));
- }
-
- public void reply(String format, Object... arguments) {
- reply(String.format(format, arguments));
- }
-
- public void reply(MessageEmbed... embeds) {
- reply(MessageCreateData.fromEmbeds(embeds));
- }
-
- public void reply(MessageCreateData msg) {
- if (channel != null && channel.canTalk()) {
- channel.sendMessage(msg).queue();
- }
- }
-
- @Override
- public void replyToMessage(String message) {
- replyToMessage(MessageCreateData.fromContent(message));
- }
-
- @Override
- public void replyToMessage(String format, Object... arguments) {
- replyToMessage(String.format(format, arguments));
- }
-
- public void replyToMessage(MessageCreateData msg) {
- if (channel != null && channel.canTalk() && message != null) {
- message.reply(msg).queue();
- }
- }
-
- public void replyPrivate(String message) {
- replyPrivate(MessageCreateData.fromContent(message));
- }
-
- public void replyPrivate(MessageCreateData msg) {
- user.openPrivateChannel().flatMap(channel -> channel.sendMessage(msg)).queue();
- }
-
- public void replyPrivate(MessageEmbed... embeds) {
- replyPrivate(MessageCreateData.fromEmbeds(embeds));
- }
-
- public void replyPrivate(Collection extends MessageEmbed> embeds) {
- user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embeds)).queue();
- }
-
- @Override
- public void deleteMessage() {
- if (message != null) {
- message.delete().queueAfter(1, TimeUnit.SECONDS);
- }
- }
-
- public boolean isMessageFromOwner() {
- return member.isOwner();
- }
-
- public boolean isMessageFromCreator() {
- String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER");
-
- return creatorId.equals(member.getId());
- }
-
-}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java b/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java
new file mode 100644
index 0000000..cf33c24
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/MessageContext.java
@@ -0,0 +1,45 @@
+package me.bobert.netneutrality.core.context;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.AsyncContext;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.interactions.components.LayoutComponent;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+public interface MessageContext extends NoMessageContext {
+ Message getMessage();
+
+ @Override
+ default boolean isFromGuild() {
+ return getMessage().isFromGuild();
+ }
+
+ @Override
+ default void deleteMessage() {
+ if (getMessage() != null && getType() != ChannelType.PRIVATE) {
+ getMessage().delete().queueAfter(1, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ default void reply(MessageCreateData messageCreateData, boolean ephemeral, List components, Consumer consumer) {
+ if (ephemeral && getType() != ChannelType.PRIVATE) {
+ sendMessageInPrivate(messageCreateData, components);
+ return;
+ }
+
+ if (getChannel() != null && getChannel().canTalk()) {
+ getMessage().reply(messageCreateData).setComponents(components).queue(message -> consumer.accept(new AsyncContext(message)));
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java b/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java
new file mode 100644
index 0000000..1aeea14
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/NoMessageContext.java
@@ -0,0 +1,47 @@
+package me.bobert.netneutrality.core.context;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.AsyncContext;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.interactions.InteractionHook;
+import net.dv8tion.jda.api.interactions.modals.Modal;
+
+import java.util.function.Consumer;
+
+public interface NoMessageContext extends Context {
+
+ @Override
+ default InteractionHook getInteractionHook() {
+ return null;
+ }
+
+ @Override
+ default boolean isAcknowledged() {
+ return true;
+ }
+
+ @Override
+ default ChannelType getType() {
+ return getChannel().getType();
+ }
+
+ @Override
+ default void reply(Modal modal) {
+ // only available for interactions
+ }
+
+ @Override
+ default void defer(boolean ephemeral, Consumer consumer) {
+ reply("Bot is thinking...", consumer);
+ }
+
+ @Override
+ default void ack(boolean ephemeral) {
+
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java
deleted file mode 100644
index 35064c1..0000000
--- a/src/main/java/me/bobert/netneutrality/core/context/SlashCommandContext.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package me.bobert.netneutrality.core.context;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.command.AbstractCommand;
-import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.User;
-import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
-import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
-import net.dv8tion.jda.api.interactions.InteractionHook;
-import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
-import net.dv8tion.jda.api.utils.messages.MessageCreateData;
-import net.dv8tion.jda.api.utils.messages.MessageEditData;
-
-import java.util.Collection;
-
-public class SlashCommandContext implements CommandContext {
-
- private final SlashCommandInteractionEvent event;
- private AbstractCommand command;
- private final JDA jda;
- private final Guild guild;
- private final MessageChannel channel;
- private final Member member;
- private final User user;
-
- private SlashCommandContext(JDA jda, SlashCommandInteractionEvent event, Guild guild, MessageChannel channel, Member member, User user) {
- this.jda = jda;
- this.event = event;
- this.guild = guild;
- this.channel = channel;
- this.member = member;
- this.user = user;
- }
-
- public static SlashCommandContext createContext(SlashCommandInteractionEvent event) {
- Member member = event.getMember();
- User user = event.getUser();
- Guild guild = event.getGuild();
- MessageChannel channel = event.getChannel();
- JDA jda = event.getJDA();
-
- return new SlashCommandContext(jda, event, guild, channel, member, user);
- }
-
- @Override
- public SlashCommandInteractionEvent getEvent() {
- return event;
- }
-
- @Override
- public AbstractCommand getCommand() {
- return command;
- }
-
- @Override
- public JDA getJDA() {
- return jda;
- }
-
- @Override
- public Guild getGuild() {
- return guild;
- }
-
- @Override
- public MessageChannel getChannel() {
- return channel;
- }
-
- @Override
- public Member getGuildUser() {
- return member;
- }
-
- @Override
- public User getDiscordUser() {
- return user;
- }
-
- @Override
- public void deleteMessage() {
- // not implemented
- }
-
- public void sendMessageInPrivate(String msg) {
- sendMessageInPrivate(new MessageCreateBuilder().setContent(msg).build());
- }
-
- public void sendMessageInPrivate(MessageCreateData msg) {
- user.openPrivateChannel().flatMap(channel -> channel.sendMessage(msg)).queue();
- }
-
- public void sendMessageInPrivate(MessageEmbed embed, MessageEmbed... embeds) {
- user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embed, embeds)).queue();
- }
-
- public void sendMessageInPrivate(Collection extends MessageEmbed> embeds) {
- user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embeds)).queue();
- }
-
- public void reply(MessageCreateData message) {
- if (event.isAcknowledged()) {
- getHook().editOriginal(MessageEditData.fromCreateData(message)).queue();
- //getHook().sendMessage(message).setEphemeral(false).queue();
- return;
- }
-
- event.reply(message).queue();
- }
-
- public void reply(MessageEmbed... embeds) {
- reply(MessageCreateData.fromEmbeds(embeds));
- }
-
- @Override
- public void reply(String message) {
- reply(MessageCreateData.fromContent(message));
- }
-
- public void replyEphemeral(MessageCreateData message) {
- if (event.isAcknowledged()) {
- getHook().editOriginal(MessageEditData.fromCreateData(message)).queue();
- //getHook().sendMessage(message).setEphemeral(true).queue();
- return;
- }
-
- event.reply(message).setEphemeral(true).queue();
- }
-
- public void replyEphemeral(MessageEmbed... embeds) {
- replyEphemeral(MessageCreateData.fromEmbeds(embeds));
- }
-
- public void replyEphemeral(String format, Object... arguments) {
- replyEphemeral(String.format(format, arguments));
- }
-
- public void replyEphemeral(String message) {
- replyEphemeral(MessageCreateData.fromContent(message));
- }
-
- @Override
- public void replyToMessage(String message) {
- replyEphemeral(message);
- }
-
- public InteractionHook getHook() {
- if (!event.isAcknowledged())
- throw new RuntimeException("Interaction isn't acknowledged yet");
-
- return event.getHook();
- }
-
- public void ackAndWait(boolean ephemeral) {
- if (event.isAcknowledged())
- throw new RuntimeException("Interaction is already acknowledged, please use getHook() instead.");
-
- event.deferReply(ephemeral).queue();
- }
-
- public void ackAndWait() {
- ackAndWait(false);
- }
-
- public void ack() {
- ackAndWait();
- }
-
- public void ephemeral() {
- ackAndWait(true);
- }
-
-}
diff --git a/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java b/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java
new file mode 100644
index 0000000..185ea57
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/context/TaskContext.java
@@ -0,0 +1,39 @@
+package me.bobert.netneutrality.core.context;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public abstract class TaskContext extends AbstractContext {
+ private final Map contextData;
+
+ public TaskContext(JDA jda, Guild guild, MessageChannel channel, Member member, User user) {
+ super(jda, guild, channel, member, user);
+ this.contextData = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public void deleteMessage() {
+
+ }
+
+ public abstract String getId();
+
+ public void addContextData(String key, Object value) {
+ contextData.put(key, value);
+ }
+
+ public Object getContextData(String key) {
+ return contextData.get(key);
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java b/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java
new file mode 100644
index 0000000..fe846e5
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/db/DatabaseMigration.java
@@ -0,0 +1,34 @@
+package me.bobert.netneutrality.core.db;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.utils.Settings;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.flywaydb.core.Flyway;
+
+public class DatabaseMigration {
+ private final static Logger logger = LogManager.getLogger(DatabaseMigration.class);
+
+ public static void migrate() {
+ String dbName = Settings.getInstance().get("MYSQL_DATABASE", "netneutrality");
+ String dbUser = Settings.getInstance().get("MYSQL_USER", "root");
+ String dbPass = Settings.getInstance().get("MYSQL_PASSWORD", "");
+ String dbHost = Settings.getInstance().get("MYSQL_HOST", "localhost");
+
+ try {
+ Flyway flyway = Flyway.configure()
+ .dataSource(String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName), dbUser, dbPass)
+ .baselineOnMigrate(true)
+ .load();
+
+ flyway.migrate();
+ } catch (Exception e) {
+ logger.error(e);
+ System.exit(1);
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/SessionManager.java b/src/main/java/me/bobert/netneutrality/core/db/SessionManager.java
similarity index 86%
rename from src/main/java/me/bobert/netneutrality/core/SessionManager.java
rename to src/main/java/me/bobert/netneutrality/core/db/SessionManager.java
index fab89e5..110a13f 100644
--- a/src/main/java/me/bobert/netneutrality/core/SessionManager.java
+++ b/src/main/java/me/bobert/netneutrality/core/db/SessionManager.java
@@ -1,4 +1,4 @@
-package me.bobert.netneutrality.core;
+package me.bobert.netneutrality.core.db;
/*
* Copyright (c) anthorob
*
@@ -20,7 +20,7 @@ import java.util.HashMap;
import java.util.Map;
public class SessionManager {
- private final static Logger logger = LogManager.getLogger(SessionManager.class);
+ private static final Logger logger = LogManager.getLogger(SessionManager.class);
private static StandardServiceRegistry registry;
private static SessionFactory sessionFactory;
@@ -35,9 +35,9 @@ public class SessionManager {
String dbHost = Settings.getInstance().get("MYSQL_HOST", "localhost");
logger.info(String.format("Connecting to database '%s' at %s:3306 with user '%s'", dbName, dbHost, dbUser));
- settings.put(Environment.URL, String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName));
- settings.put(Environment.USER, dbUser);
- settings.put(Environment.PASS, dbPass);
+ settings.put(Environment.JAKARTA_JDBC_URL, String.format("jdbc:mysql://%s:3306/%s", dbHost, dbName));
+ settings.put(Environment.JAKARTA_JDBC_USER, dbUser);
+ settings.put(Environment.JAKARTA_JDBC_PASSWORD, dbPass);
registryBuilder.applySettings(settings);
registry = registryBuilder.build();
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java
new file mode 100644
index 0000000..f334bf4
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractBuiltInTask.java
@@ -0,0 +1,40 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+import me.bobert.netneutrality.core.context.TaskContext;
+
+public abstract class AbstractBuiltInTask extends AbstractTask implements BuiltInTask {
+
+ @Override
+ public String getName() {
+ return getId();
+ }
+
+ @Override
+ public boolean isInsertable() {
+ return true;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return false;
+ }
+
+ @Override
+ public void replyToInteraction(CommandContext context) {
+ // Task is loaded at runtime, this will never be called
+ }
+
+ @Override
+ public Lifecycle execute(TaskContext context) {
+ executeTask(context);
+ return Lifecycle.BUILTIN;
+ }
+
+ public abstract void executeTask(TaskContext context);
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java
new file mode 100644
index 0000000..4b03f13
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractPersistableTask.java
@@ -0,0 +1,58 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+import org.json.JSONObject;
+
+public abstract class AbstractPersistableTask extends AbstractTask implements PersistableTask {
+
+ @Override
+ public boolean isInsertable() {
+ return true;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return false;
+ }
+
+ @Override
+ public void replyToInteraction(CommandContext context) {
+ // Task loaded from database, this will never be called
+ }
+
+ @Override
+ public void insert(CommandContext context) {
+ if (TaskManager.getInstance().hasTask(getId())) {
+ throw new RuntimeException("Cannot insert a task that already exists");
+ }
+
+ TaskManager.getInstance().insert(context, this);
+ }
+
+ @Override
+ public void save() {
+ if (!TaskManager.getInstance().hasTask(getId())) {
+ throw new RuntimeException("Cannot save a task that doesn't exist yet");
+ }
+
+ TaskManager.getInstance().getTaskService().persistTask(this);
+ }
+
+ @Override
+ public void delete() {
+ TaskManager.getInstance().getTaskService().unpersistTask(this);
+ TaskManager.getInstance().removeTask(this);
+ }
+
+ @Override
+ public String toPayload() {
+ JSONObject payload = new JSONObject(this);
+
+ return payload.toString();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java
new file mode 100644
index 0000000..ba5d5a1
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/AbstractTask.java
@@ -0,0 +1,48 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+
+import java.time.OffsetDateTime;
+import java.util.Objects;
+
+public abstract class AbstractTask implements Task {
+ private final OffsetDateTime creationTime;
+
+ public AbstractTask() {
+ creationTime = OffsetDateTime.now();
+ }
+
+ @Override
+ public OffsetDateTime getCreationTime() {
+ return creationTime;
+ }
+
+ @Override
+ public void insert(CommandContext context) {
+ TaskManager.getInstance().insert(context, this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AbstractTask that = (AbstractTask) o;
+ return Objects.equals(getId(), that.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(getId());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Task{id=%s, lockId=%s, class=%s, name=%s, creationTime=%s}", getId(), getLockId(), getClass().getSimpleName(), getName(), getCreationTime());
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java
new file mode 100644
index 0000000..a64fa2d
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/BuiltInTask.java
@@ -0,0 +1,25 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.interactions.components.buttons.Button;
+
+/**
+ * Represents a task that has a known ID and is loaded at runtime.
+ * This can be any task and is not necessarily a button.
+ */
+public interface BuiltInTask extends Task {
+
+ static Button createButton(Class extends BuiltInTask> clazz) {
+ BuiltInTask builtInTask = TaskManager.getInstance().findBuiltInTaskByClass(clazz);
+
+ if (builtInTask == null) {
+ throw new IllegalArgumentException("Task class " + clazz.getName() + " was not registered by the task manager.");
+ }
+
+ return builtInTask.createButton();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java
new file mode 100644
index 0000000..8ab28a2
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/GenericCustomIdTask.java
@@ -0,0 +1,27 @@
+package me.bobert.netneutrality.core.tasks;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import net.dv8tion.jda.api.interactions.components.buttons.Button;
+
+public abstract class GenericCustomIdTask extends AbstractTask {
+ private final String customId;
+
+ public GenericCustomIdTask(String id) {
+ this.customId = id;
+ }
+
+ @Override
+ public String getId() {
+ return customId;
+ }
+
+ @Override
+ public Button createButton() {
+ return null;
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java
new file mode 100644
index 0000000..23cde6e
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/GenericRandomIdTask.java
@@ -0,0 +1,15 @@
+package me.bobert.netneutrality.core.tasks;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import java.util.UUID;
+
+public abstract class GenericRandomIdTask extends GenericCustomIdTask {
+ public GenericRandomIdTask() {
+ super(UUID.randomUUID().toString());
+ }
+}
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/PersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java
new file mode 100644
index 0000000..a5a1cc7
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java
@@ -0,0 +1,23 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+
+/**
+ * Represents a task that can be persisted and restored later.
+ * The persistable task provides functionality to serialize its state
+ * so that it can be stored in a database.
+ */
+public interface PersistableTask extends Task {
+ void insert(CommandContext context);
+
+ void save();
+
+ void delete();
+
+ String toPayload();
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/Task.java b/src/main/java/me/bobert/netneutrality/core/tasks/Task.java
new file mode 100644
index 0000000..bc2a084
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/Task.java
@@ -0,0 +1,79 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+import me.bobert.netneutrality.core.context.TaskContext;
+import me.bobert.netneutrality.core.tasks.trait.WithChildren;
+import net.dv8tion.jda.api.interactions.components.buttons.Button;
+
+import java.time.OffsetDateTime;
+
+public interface Task {
+ enum Lifecycle {
+ BUILTIN,
+ PARENT,
+ KEEP,
+ REMOVE_SELF,
+ REMOVE_SELF_AND_CHILDREN
+ }
+ /**
+ * This method should generate a unique ID to differentiate it from other tasks.
+ *
+ * This determines what triggers a task to be executed.
+ * For example, a modal or button would use the component id.
+ * For an emoji reaction, the guild and message id would be combined.
+ *
+ *
+ * @return a unique ID for this task
+ */
+ String getId();
+
+ /**
+ * Retrieves the lock ID that is used to synchronize tasks
+ * that require a locking mechanism.
+ *
+ * @return the lock ID associated with this task
+ */
+ default String getLockId() {
+ return getId();
+ }
+
+ /**
+ * This is only used for debugging purpose.
+ * @return Name of the task
+ */
+ String getName();
+
+ /**
+ * If this returns false, the task will not get inserted and executed.
+ */
+ boolean isInsertable();
+
+ /**
+ * When this returns true, the task will get removed by the cleanup thread.
+ * @return If the task is expired
+ */
+ boolean isExpired();
+
+ OffsetDateTime getCreationTime();
+
+ Button createButton();
+
+ /**
+ * This method is used to reply to the interaction that triggered this task.
+ * If the task is implementing {@link WithChildren}, the child tasks must be instanciated in this method.
+ */
+ void replyToInteraction(CommandContext context);
+
+ /**
+ * Executes the task.
+ * @param context The context for this task.
+ */
+ Lifecycle execute(TaskContext context);
+
+ void insert(CommandContext context);
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java
new file mode 100644
index 0000000..cd13aa0
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/TaskManager.java
@@ -0,0 +1,315 @@
+package me.bobert.netneutrality.core.tasks;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+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.trait.Deletable;
+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.thread.ThreadManager;
+import me.bobert.netneutrality.db.service.TaskService;
+import me.bobert.netneutrality.exceptions.GracefulContextError;
+import me.bobert.netneutrality.exceptions.TaskExecutionException;
+import me.bobert.netneutrality.thread.CleanupTaskThread;
+import net.dv8tion.jda.api.interactions.InteractionHook;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.reflections.Reflections;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+public class TaskManager {
+ private static final Logger logger = LogManager.getLogger(TaskManager.class);
+ private static TaskManager instance;
+ private final TaskService taskService;
+ private final Map tasks;
+ private final List pendingTasks;
+ private final Map deletableTasksContext;
+ private final Map tasksLock;
+
+ private TaskManager() {
+ this.tasks = new ConcurrentHashMap<>();
+ this.pendingTasks = new CopyOnWriteArrayList<>();
+ this.deletableTasksContext = new ConcurrentHashMap<>();
+ this.tasksLock = new ConcurrentHashMap<>();
+ this.taskService = new TaskService();
+ }
+
+ public static TaskManager getInstance() {
+ if (instance == null) {
+ instance = new TaskManager();
+ }
+
+ return instance;
+ }
+
+ public void init() {
+ loadBuiltInTasks();
+ taskService.loadTasks();
+
+ ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new CleanupTaskThread(), 1, 5, TimeUnit.MINUTES);
+ }
+
+ protected TaskService getTaskService() {
+ return taskService;
+ }
+
+ public Task getTask(String id) {
+ return tasks.get(id);
+ }
+
+ public boolean hasTask(String id) {
+ return tasks.containsKey(id) || pendingTasks.stream().anyMatch(task -> task.getId().equals(id));
+ }
+
+ public synchronized Collection getTasks() {
+ return tasks.values();
+ }
+
+ public void insert(CommandContext context, Task task) {
+ String id = task.getId();
+
+ if (hasTask(id)) {
+ context.reply("A task with the same ID already exists!");
+ return;
+ }
+
+ task.replyToInteraction(context);
+
+ if (task.isInsertable()) {
+ logger.info("Inserting task: {}", task);
+
+ if (task instanceof PersistableTask persistableTask) {
+ taskService.persistTask(persistableTask);
+ }
+
+ if (task instanceof IStepTask) {
+ pendingTasks.add(task);
+ } else {
+ tasks.put(id, task);
+ }
+ }
+
+ if (task instanceof Deletable) {
+ deletableTasksContext.put(task.getId(), context);
+ }
+
+ if (task instanceof WithChildren taskWithChildren) {
+ List childrens = taskWithChildren.getChildTasks();
+ if (childrens == null || childrens.isEmpty()) {
+ logger.warn("Parent task '{}' doesn't create any childs", task.getName());
+ return;
+ }
+
+ for (Task child : childrens) {
+ insert(context, child);
+ }
+ }
+ }
+
+ public void activateTask(Task task, String oldId) {
+ if (pendingTasks.remove(task)) {
+ tasks.put(task.getId(), task);
+ logger.info("Updating task {} with new id: {} (was: {})",
+ task.getName(), task.getId(), oldId);
+ }
+ }
+
+ public void removeTask(Task task) {
+ String id = task.getId();
+
+ if (tasks.remove(id) != null) {
+ logger.debug("Removing task: {}", id);
+
+ ReentrantLock lock = tasksLock.remove(task.getLockId());
+ if (lock != null) {
+ logger.debug("Cleaning up lock for removed task: {}", task.getId());
+ }
+ }
+ }
+
+ public void handle(TaskContext context) {
+ String taskId = context.getId();
+ Task task = getTask(taskId);
+
+ if (task == null && context instanceof InteractionContext) {
+ context.replyEphemeral("This task is no longer available, someone might have already used it or it might have expired");
+ }
+
+ if (task != null) {
+ String lockId = task.getLockId();
+ ReentrantLock lock = tasksLock.computeIfAbsent(lockId, k -> {
+ if (task instanceof PersistableTask || task instanceof BuiltInTask) {
+ return null;
+ }
+
+ return new ReentrantLock();
+ });
+
+ try {
+ if (lock != null) {
+ logger.debug("Aquiring lock for task: {}", taskId);
+ lock.lock();
+
+ if (!tasks.containsKey(taskId)) {
+ logger.debug("Task {} is no longer available after aquiring lock", taskId);
+ tasksLock.remove(lockId);
+ return;
+ }
+ }
+
+ logger.debug("Executing task: {}", taskId);
+ Task.Lifecycle status = task.execute(context);
+ handleTaskLifecycle(task, status);
+
+ deleteInteraction(task);
+
+ // If interaction is still not acknowledged after execution, send a placeholder response.
+ if (!context.isAcknowledged() && !(task instanceof NotAcknowledgeable)) {
+ logger.warn("Interaction not acknowledged");
+ context.replyEphemeral("Task executed successfully but interaction wasn't acknowledged, report this bug by using the command /report-bug");
+ }
+ } catch (GracefulContextError exception) {
+ logger.error(exception.getMessage());
+ context.replyEphemeral(exception.getMessage());
+ } catch (TaskExecutionException exception) {
+ LogHandler.log(exception);
+ } catch (Exception exception) {
+ LogHandler.log(new TaskExecutionException("Error during task execution", context, exception));
+ } finally {
+ if (lock != null) {
+ logger.debug("Releasing lock for task: {}", taskId);
+ lock.unlock();
+ }
+ }
+ }
+ }
+
+ 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)
+ .map(clazz::cast)
+ .filter(task -> task.getId().contains(partialId))
+ .collect(Collectors.toList());
+ }
+
+ public BuiltInTask findBuiltInTaskByClass(Class extends BuiltInTask> clazz) {
+ return tasks.values().stream()
+ .filter(clazz::isInstance)
+ .map(task -> (BuiltInTask) task)
+ .findFirst()
+ .orElse(null);
+ }
+
+ public void insertPersistableTask(PersistableTask task) {
+ if (tasks.containsKey(task.getId())) {
+ logger.error("Trying to load persistable task but ID already exists: {}", task.getId());
+ return;
+ }
+
+ logger.info("Loaded persistable task: {}", task);
+ tasks.put(task.getId(), task);
+ }
+
+ private void loadBuiltInTasks() {
+ logger.info("Loading built-in tasks...");
+
+ Set> classes = new Reflections("me.bobert.netneutrality.tasks").getSubTypesOf(BuiltInTask.class);
+ for (Class extends BuiltInTask> s : classes) {
+ try {
+ if (Modifier.isAbstract(s.getModifiers())) {
+ continue;
+ }
+
+ BuiltInTask task = s.getConstructor().newInstance();
+
+ logger.info("Loaded task: {}", task);
+ tasks.put(task.getId(), task);
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+ logger.error("Failed to instantiate built-in task", e);
+ }
+ }
+ }
+
+ private void deleteInteraction(Task task) {
+ if (task instanceof WithParent> taskWithParent) {
+ Task parent = taskWithParent.getParentTask();
+
+ if (parent instanceof Deletable deletable && task.isInsertable()) {
+ deleteInteraction(parent.getId(), deletable);
+ return;
+ }
+ }
+
+ if (task instanceof Deletable deletable && task.isInsertable()) {
+ deleteInteraction(task.getId(), deletable);
+ }
+ }
+
+ private void deleteInteraction(String taskId, Deletable deletable) {
+ logger.debug("Deleting interaction: {}", deletable);
+
+ CommandContext context = deletableTasksContext.get(taskId);
+ if (context != null) {
+ deletableTasksContext.remove(taskId);
+
+ InteractionHook hook = context.getInteractionHook();
+ if (hook != null) {
+ if (hook.isExpired()) {
+ context.replyEphemeral("This interaction has expired, can't remove it");
+ }
+
+ if (!hook.isExpired()) {
+ hook.deleteOriginal().queue();
+ }
+ }
+ }
+
+ deletable.onInteractionDelete();
+ }
+
+ private void handleTaskLifecycle(Task task, Task.Lifecycle status) {
+ if (status == null) {
+ throw new IllegalArgumentException("Task lifecycle status cannot be null");
+ }
+
+ switch (status) {
+ case REMOVE_SELF:
+ removeTask(task);
+ break;
+
+ case REMOVE_SELF_AND_CHILDREN:
+ if (task instanceof WithChildren taskWithChildren) {
+ for (Task child : taskWithChildren.getChildTasks()) {
+ removeTask(child);
+ }
+ }
+ removeTask(task);
+ break;
+
+ case KEEP:
+ default:
+ break;
+ }
+ }
+}
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
new file mode 100644
index 0000000..f18d76e
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ButtonTask.java
@@ -0,0 +1,53 @@
+package me.bobert.netneutrality.core.tasks.impl;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+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;
+ private final String buttonLabel;
+
+ public ButtonTask(ButtonStyle buttonStyle, String buttonLabel) {
+ this.buttonStyle = buttonStyle;
+ this.buttonLabel = buttonLabel;
+ }
+
+ @Override
+ public Button createButton() {
+ if (buttonStyle == null || buttonLabel == null) {
+ return null;
+ }
+
+ return Button.of(buttonStyle, getId(), buttonLabel);
+ }
+
+ @Override
+ 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
new file mode 100644
index 0000000..dae4604
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java
@@ -0,0 +1,62 @@
+package me.bobert.netneutrality.core.tasks.impl;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
+import me.bobert.netneutrality.core.tasks.AbstractPersistableTask;
+import net.dv8tion.jda.api.interactions.components.buttons.Button;
+
+import java.util.Map;
+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 channelId, String messageId) {
+ this.guildId = guildId;
+ this.channelId = channelId;
+ this.messageId = messageId;
+ }
+
+ public String getGuildId() {
+ 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() {
+ return emojiMapping;
+ }
+
+ @Override
+ public String getId() {
+ return String.format(EmojiTaskContext.ID, getGuildId(), getMessageId());
+ }
+
+ @Override
+ public Button createButton() {
+ return null;
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java
new file mode 100644
index 0000000..94db7a9
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/ModalTask.java
@@ -0,0 +1,30 @@
+package me.bobert.netneutrality.core.tasks.impl;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.TaskContext;
+import me.bobert.netneutrality.core.tasks.GenericRandomIdTask;
+import net.dv8tion.jda.api.interactions.modals.ModalMapping;
+
+import java.time.OffsetDateTime;
+import java.util.List;
+
+public abstract class ModalTask extends GenericRandomIdTask {
+
+ @Override
+ public boolean isExpired() {
+ return getCreationTime().plusMinutes(5).isBefore(OffsetDateTime.now());
+ }
+
+ public ModalMapping getValue(String key, TaskContext context) {
+ List> mappings = (List>) context.getContextData("values");
+ return mappings.stream()
+ .filter(o -> o instanceof ModalMapping)
+ .map(o -> (ModalMapping) o)
+ .filter(mapping -> mapping.getId().equals(key))
+ .findFirst().orElse(null);
+ }
+}
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
new file mode 100644
index 0000000..0c7f92a
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/StepTask.java
@@ -0,0 +1,169 @@
+package me.bobert.netneutrality.core.tasks.impl;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+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 net.fellbaum.jemoji.Emoji;
+import net.fellbaum.jemoji.EmojiManager;
+
+import java.time.OffsetDateTime;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * 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();
+
+ @Override
+ public String getId() {
+ 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 is of type role but it was inserted in a non-guild context");
+ }
+
+ 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) {
+ synchronized (stepLock) {
+ if (!context.getDiscordUser().equals(commandContext.getDiscordUser())) {
+ return Lifecycle.KEEP;
+ }
+ }
+
+ 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() {
+ // is it really needed to expire this type of task?
+ return getCreationTime().plusMinutes(60).isBefore(OffsetDateTime.now());
+ }
+
+ @Override
+ public Button createButton() {
+ return null;
+ }
+
+ /**
+ * 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) {
+ if (newId == null || newId.isBlank()) {
+ throw new IllegalArgumentException("Task ID cannot be null or empty.");
+ }
+
+ String oldId = id;
+ synchronized (this) {
+ this.id = newId;
+ TaskManager.getInstance().activateTask(this, oldId);
+ }
+ }
+}
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
new file mode 100644
index 0000000..80b89b1
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskFactory.java
@@ -0,0 +1,81 @@
+package me.bobert.netneutrality.core.tasks.persistence;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.tasks.PersistableTask;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.Map;
+
+public class TaskFactory {
+ public enum ParamType {
+ STRING(String.class),
+ INT(Integer.class),
+ MAP(Map.class);
+
+ private final Class> typeClass;
+
+ ParamType(Class> typeClass) {
+ this.typeClass = typeClass;
+ }
+
+ public Class> getTypeClass() {
+ return typeClass;
+ }
+
+ public static ParamType fromString(String type) {
+ return switch (type.toLowerCase()) {
+ case "string" -> STRING;
+ case "int" -> INT;
+ case "map" -> MAP;
+ default -> throw new IllegalArgumentException("Unsupported type: " + type);
+ };
+ }
+ }
+
+ 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 found in task mappings");
+ }
+
+ Class> clazz = Class.forName(className);
+ if (!PersistableTask.class.isAssignableFrom(clazz)) {
+ throw new IllegalArgumentException("Class does not implement PersistableTask.");
+ }
+
+ Class>[] classTypes = new Class>[types.length()];
+ Object[] classParams = new Object[params.length()];
+
+ for (int i = 0; i < params.length(); i++) {
+ ParamType paramType = ParamType.fromString(types.getString(i));
+ String paramName = params.getString(i);
+
+ classTypes[i] = paramType.getTypeClass();
+ classParams[i] = extractValue(paramType, paramName, payload);
+ }
+
+ try {
+ return (PersistableTask) clazz.getConstructor(classTypes).newInstance(classParams);
+ } catch (NoSuchMethodException e) {
+ throw new NoSuchMethodException("No matching constructor found with parameters: " + Arrays.toString(classTypes));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid arguments for constructor. Expected: " + Arrays.toString(classParams), e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Constructor threw an exception", e.getCause());
+ }
+ }
+
+ private static Object extractValue(ParamType type, String paramName, JSONObject payload) {
+ return switch (type) {
+ case STRING -> payload.getString(paramName);
+ case INT -> payload.getInt(paramName);
+ case MAP -> payload.getJSONObject(paramName).toMap();
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java
new file mode 100644
index 0000000..e0dc7b3
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/TaskRegistry.java
@@ -0,0 +1,94 @@
+package me.bobert.netneutrality.core.tasks.persistence;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.tasks.PersistableTask;
+import org.apache.commons.io.IOUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TaskRegistry {
+ @FunctionalInterface
+ public interface TaskCreator {
+ PersistableTask fromPayload(JSONObject payload);
+ }
+
+ private static final Logger logger = LogManager.getLogger(TaskRegistry.class);
+ private static final Map taskCreators = new HashMap<>();
+ private static final String FILE_NAME = "persistable-tasks.json";
+
+ // JSON keys as constants
+ private static final String KEY_TASKS = "tasks";
+ private static final String KEY_CLASS = "class";
+ private static final String KEY_PARAMS = "params";
+ private static final String KEY_TYPES = "types";
+
+ public static TaskCreator getTaskCreator(String className) {
+ return taskCreators.get(className);
+ }
+
+ public static void loadTaskConfig() {
+ logger.info("Reading task mappings from {}..", FILE_NAME);
+
+ try (InputStream is = TaskFactory.class.getClassLoader().getResourceAsStream(FILE_NAME)) {
+ if (is == null) {
+ throw new FileNotFoundException("Could not load: " + FILE_NAME);
+ }
+
+ String jsonString = IOUtils.toString(is, StandardCharsets.UTF_8);
+ JSONObject json = new JSONObject(jsonString);
+ JSONArray tasksArray = json.optJSONArray(KEY_TASKS);
+
+ if (tasksArray == null) {
+ logger.warn("No tasks found in JSON: {}", FILE_NAME);
+ return;
+ }
+
+ for (int i = 0; i < tasksArray.length(); i++) {
+ JSONObject taskJson = tasksArray.getJSONObject(i);
+
+ String className = taskJson.optString(KEY_CLASS, "").trim();
+ if (className.isEmpty()) {
+ logger.warn("Missing JSON key '{}' at index {}", KEY_CLASS, i);
+ continue;
+ }
+
+ JSONArray params = taskJson.optJSONArray(KEY_PARAMS);
+ if (params == null) {
+ logger.warn("Missing JSON key '{}' for class: {}", KEY_PARAMS, className);
+ continue;
+ }
+
+ JSONArray types = taskJson.optJSONArray(KEY_TYPES);
+ if (types == null) {
+ logger.warn("Missing JSON key '{}' for class: {}", KEY_TYPES, className);
+ continue;
+ }
+
+ taskCreators.put(className, payload -> {
+ try {
+ return TaskFactory.createTaskInstance(className, params, types, payload);
+ } catch (Exception e) {
+ logger.error("Failed to create instance of '{}': {}", className, e.getMessage());
+ return null;
+ }
+ });
+ }
+
+ logger.info("{} mappings registered", taskCreators.size());
+ } catch (Exception e) {
+ logger.error(e);
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java
new file mode 100644
index 0000000..661a8cd
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/Deletable.java
@@ -0,0 +1,19 @@
+package me.bobert.netneutrality.core.tasks.trait;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+/**
+ * Represents a task that will remove its interaction from the channel after execution
+ */
+public interface Deletable {
+
+ /**
+ * This method is called after the interaction associated with a deletable task
+ * has been deleted. Tasks that implement the {@link Deletable} interface can provide
+ * their own cleanup logic in this method.
+ */
+ void onInteractionDelete();
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java
new file mode 100644
index 0000000..e3c3b75
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/NotAcknowledgeable.java
@@ -0,0 +1,12 @@
+package me.bobert.netneutrality.core.tasks.trait;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+/**
+ * Represents a task that will not acknowledge the interaction when executed.
+ */
+public interface NotAcknowledgeable {
+}
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
new file mode 100644
index 0000000..22cf018
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithChildren.java
@@ -0,0 +1,29 @@
+package me.bobert.netneutrality.core.tasks.trait;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.TaskContext;
+import me.bobert.netneutrality.core.tasks.Task;
+
+import java.util.List;
+
+public interface WithChildren extends Task {
+
+ @Override
+ default Lifecycle execute(TaskContext context) {
+ return Lifecycle.PARENT;
+ }
+
+ List getChildTasks();
+
+ default void addChild(List tasks) {
+ getChildTasks().addAll(tasks);
+ }
+
+ default void addChild(Task... tasks) {
+ addChild(List.of(tasks));
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java
new file mode 100644
index 0000000..026dbaf
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/core/tasks/trait/WithParent.java
@@ -0,0 +1,34 @@
+package me.bobert.netneutrality.core.tasks.trait;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.CommandContext;
+import me.bobert.netneutrality.core.tasks.Task;
+
+public interface WithParent extends Task {
+
+ /**
+ * Retrieves the lock ID associated with the parent task.
+ *
+ * @return the lock ID of the parent task
+ */
+ @Override
+ default String getLockId() {
+ return getParentTask().getLockId();
+ }
+
+ @Override
+ default void replyToInteraction(CommandContext context) {
+ // Child tasks shouldn't reply to the interaction, this is the job of the parent task
+ }
+
+ /**
+ * Retrieves the parent task associated with this task.
+ *
+ * @return the parent task of type E
+ */
+ E getParentTask();
+}
diff --git a/src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java b/src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java
similarity index 94%
rename from src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java
rename to src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java
index 1c33201..d0b5c16 100644
--- a/src/main/java/me/bobert/netneutrality/thread/BaseThreadFactory.java
+++ b/src/main/java/me/bobert/netneutrality/core/thread/BaseThreadFactory.java
@@ -1,4 +1,4 @@
-package me.bobert.netneutrality.thread;
+package me.bobert.netneutrality.core.thread;
/*
* Copyright (c) anthorob
diff --git a/src/main/java/me/bobert/netneutrality/thread/ThreadManager.java b/src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java
similarity index 55%
rename from src/main/java/me/bobert/netneutrality/thread/ThreadManager.java
rename to src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java
index f2d88e7..a08fb9e 100644
--- a/src/main/java/me/bobert/netneutrality/thread/ThreadManager.java
+++ b/src/main/java/me/bobert/netneutrality/core/thread/ThreadManager.java
@@ -1,4 +1,4 @@
-package me.bobert.netneutrality.thread;
+package me.bobert.netneutrality.core.thread;
/*
* Copyright (c) anthorob
*
@@ -10,40 +10,24 @@ import java.util.concurrent.*;
public class ThreadManager {
- public enum ExecutorType {
- SINGLE, SCHEDULED, FIXED
- }
-
private static final Map executors = new ConcurrentHashMap<>();
- public static ScheduledExecutorService getMusicExecutor() {
+ public static ScheduledExecutorService getScheduledExecutor() {
return (ScheduledExecutorService) executors.computeIfAbsent(
- "music-executor",
+ "scheduled-executor",
s -> Executors.newScheduledThreadPool(
Runtime.getRuntime().availableProcessors(),
- new BaseThreadFactory("music-executor")
+ new BaseThreadFactory(s)
)
);
}
- public static ExecutorService getExecutor() {
- return executors.computeIfAbsent("general-executor", s -> Executors.newCachedThreadPool(new BaseThreadFactory("general-executor")));
- }
-
public static ExecutorService getExecutor(String name) {
- return executors.computeIfAbsent(name, s -> Executors.newSingleThreadExecutor(new BaseThreadFactory(name)));
+ return executors.computeIfAbsent(name, s -> Executors.newCachedThreadPool(new BaseThreadFactory(s)));
}
- public static ExecutorService getExecutor(String name, ExecutorType type) {
- ExecutorService executor;
-
- switch (type) {
- case FIXED -> executor = executors.computeIfAbsent(name, s -> Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new BaseThreadFactory(name)));
- case SCHEDULED -> executor = executors.computeIfAbsent(name, s -> Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new BaseThreadFactory(name)));
- default -> executor = executors.computeIfAbsent(name, s -> Executors.newSingleThreadExecutor(new BaseThreadFactory(name)));
- }
-
- return executor;
+ public static ExecutorService getExecutor() {
+ return getExecutor("general-executor");
}
public static void shutdownExecutor() {
diff --git a/src/main/java/me/bobert/netneutrality/db/AbstractController.java b/src/main/java/me/bobert/netneutrality/db/AbstractController.java
deleted file mode 100644
index 6fad802..0000000
--- a/src/main/java/me/bobert/netneutrality/db/AbstractController.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package me.bobert.netneutrality.db;
-
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.SessionManager;
-import org.hibernate.SessionFactory;
-
-public abstract class AbstractController {
-
- protected SessionFactory sessionFactory;
-
- public AbstractController() {
- sessionFactory = SessionManager.getSessionFactory();
- }
-
- protected SessionFactory getSessionFactory() {
- return sessionFactory;
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/db/DBController.java b/src/main/java/me/bobert/netneutrality/db/DBController.java
deleted file mode 100644
index aab0723..0000000
--- a/src/main/java/me/bobert/netneutrality/db/DBController.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package me.bobert.netneutrality.db;
-
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.SessionManager;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.lang.reflect.InvocationTargetException;
-
-public class DBController {
-
- private final static Logger logger = LogManager.getLogger(DBController.class);
-
- public static T get(Class returnType) {
- if (returnType == null)
- throw new IllegalArgumentException("return type is null");
-
- try {
- return returnType.getConstructor().newInstance();
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static void shutdown() {
- SessionManager.shutdown();
- }
-
- public static void init() {
- SessionManager.getSessionFactory();
- }
-
-}
diff --git a/src/main/java/me/bobert/netneutrality/db/GSettings.java b/src/main/java/me/bobert/netneutrality/db/GSettings.java
index 5945ff5..64f094a 100644
--- a/src/main/java/me/bobert/netneutrality/db/GSettings.java
+++ b/src/main/java/me/bobert/netneutrality/db/GSettings.java
@@ -9,7 +9,8 @@ public enum GSettings {
//TODO: description
MUSIC_ROLE("MUSIC_ROLE", ""),
MUSIC_CHANNEL("MUSIC_CHANNEL", ""),
- MUSIC_VOLUME("MUSIC_VOLUME", "30");
+ MUSIC_VOLUME("MUSIC_VOLUME", "30"),
+ MUSIC_MAXPLAYLISTSIZE("MUSIC_MAXPLAYLISTSIZE", "20");
private final String name;
private final String defaultValue;
diff --git a/src/main/java/me/bobert/netneutrality/db/Repository.java b/src/main/java/me/bobert/netneutrality/db/Repository.java
new file mode 100644
index 0000000..09494a6
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/Repository.java
@@ -0,0 +1,44 @@
+package me.bobert.netneutrality.db;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.hibernate.Session;
+import org.hibernate.Transaction;
+import org.hibernate.exception.ConstraintViolationException;
+
+import java.util.List;
+
+public interface Repository {
+ Logger logger = LogManager.getLogger(Repository.class);
+
+ List findAll();
+
+ O findById(String id);
+
+ default void save(O entity) {
+ Transaction transaction = null;
+
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ transaction = session.beginTransaction();
+ session.merge(entity);
+ transaction.commit();
+ } catch (ConstraintViolationException e) {
+ logger.error(e);
+ throw e;
+ } catch (Exception e) {
+ logger.error(e);
+
+ if (transaction != null) {
+ transaction.rollback();
+ }
+ }
+ }
+
+ void deleteBy(String id);
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java b/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java
deleted file mode 100644
index 19dea2a..0000000
--- a/src/main/java/me/bobert/netneutrality/db/controllers/DBBlacklist.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package me.bobert.netneutrality.db.controllers;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.core.context.CommandContext;
-import me.bobert.netneutrality.db.AbstractController;
-import me.bobert.netneutrality.db.entities.OBlacklist;
-import me.bobert.netneutrality.db.entities.OGuild;
-import me.bobert.netneutrality.utils.JpaUtils;
-import net.dv8tion.jda.api.entities.Guild;
-import org.hibernate.Session;
-import org.hibernate.query.Query;
-
-import java.util.ArrayList;
-
-public class DBBlacklist extends AbstractController {
-
- public void add(String word, CommandContext context) {
- GuildManager guildManager = GuildManager.getManager(context.getGuild());
-
-
- OBlacklist bl = getWordFromDb(word, context.getGuild().getId());
-
- if (bl == null) {
- Session session = getSessionFactory().openSession();
-
- session.beginTransaction();
- bl = new OBlacklist();
- bl.setGuild(guildManager.getGuildEntity());
- bl.setCreatorId(context.getGuildUser().getId());
- bl.setWord(word);
- bl.setEnabled(true);
-
- session.persist(bl);
- session.getTransaction().commit();
- session.close();
-
- guildManager.reloadBlacklistCache();
- context.reply(String.format("Added the word [%s] to the blacklist.", word));
- } else {
- context.reply("Word is already blacklisted");
- }
-
-
-
- }
-
- public void delete(String word, CommandContext context) {
- GuildManager guildManager = GuildManager.getManager(context.getGuild());
- OBlacklist bl = getWordFromDb(word, context.getGuild().getId());
-
- if (bl != null) {
- Session session = getSessionFactory().openSession();
- session.beginTransaction();
-
- session.remove(bl);
- session.getTransaction().commit();
- session.close();
-
- guildManager.reloadBlacklistCache();
- context.reply("Removed the word [%s] from the blacklist.", word);
- } else {
- context.reply("Can't delete word because it doesn't exist");
- }
- }
-
- public void enable(String word, CommandContext context) {
- GuildManager guildManager = GuildManager.getManager(context.getGuild());
-
-
- OBlacklist bl = getWordFromDb(word, context.getGuild().getId());
- if (bl != null && !bl.getEnabled()) {
- Session session = getSessionFactory().openSession();
- session.beginTransaction();
-
- bl.setEnabled(true);
-
- session.merge(bl);
- session.getTransaction().commit();
- session.close();
-
- guildManager.reloadBlacklistCache();
- context.reply("Enabled the word [%s] for the blacklist.", word);
- } else {
- context.reply("Either word doesn't exist or is already enabled");
- }
-
-
- }
-
- public void disable(String word, CommandContext context) {
- GuildManager guildManager = GuildManager.getManager(context.getGuild());
-
-
- OBlacklist bl = getWordFromDb(word, context.getGuild().getId());
- if (bl != null && bl.getEnabled()) {
- Session session = getSessionFactory().openSession();
- session.beginTransaction();
-
- bl.setEnabled(false);
-
- session.merge(bl);
- session.getTransaction().commit();
- session.close();
-
- guildManager.reloadBlacklistCache();
- context.reply("Disabled the word [%s] for the blacklist.", word);
- } else {
- context.reply("Either word doesn't exist or is already disabled");
- }
- }
-
- public ArrayList getBlacklistForGuild(Guild guild) {
- GuildManager guildManager = GuildManager.getManager(guild);
- Session session = getSessionFactory().openSession();
-
- OGuild guildEntity = guildManager.getGuildEntity();
- JpaUtils.tryLoad(session, guildEntity, guildEntity.getGuildId());
-
- ArrayList blacklists = new ArrayList<>(guildEntity.getBlacklists());
- session.close();
-
- return blacklists;
- }
-
- private OBlacklist getWordFromDb(String word, String guildId) {
- Session session = getSessionFactory().openSession();
-
- Query query = session.createNamedQuery("OBlacklist.findByWordEqualsIgnoreCaseAndGuild_GuildIdEquals", OBlacklist.class);
- query.setParameter("guildId", guildId);
- query.setParameter("word", word);
- OBlacklist blacklist = query.uniqueResult();
-
- session.close();
- return blacklist;
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java b/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java
deleted file mode 100644
index 81b1b38..0000000
--- a/src/main/java/me/bobert/netneutrality/db/controllers/DBMessage.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package me.bobert.netneutrality.db.controllers;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.db.AbstractController;
-import me.bobert.netneutrality.db.entities.OLogMessage;
-import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.Message;
-import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.hibernate.Session;
-import org.hibernate.query.Query;
-
-import java.util.List;
-
-public class DBMessage extends AbstractController {
-
- private final static Logger logger = LogManager.getLogger(DBMessage.class);
-
- public void log(GuildManager guildManager, MessageChannel channel, Member author, Message message) {
- Session session = getSessionFactory().openSession();
- session.beginTransaction();
-
- OLogMessage logMessage = new OLogMessage();
- logMessage.setGuild(guildManager.getGuildEntity());
- logMessage.setMessageId(message.getId());
- logMessage.setMessage(message.getContentRaw().trim());
- logMessage.setChannelId(channel.getId());
- logMessage.setUserId(author.getId());
- logMessage.setDateSent(message.getTimeCreated().toInstant());
-
- session.persist(logMessage);
- session.getTransaction().commit();
- session.close();
- }
-
- public void logEdit(String messageId, String newMessage) {
- Session session = getSessionFactory().openSession();
- Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class);
- query.setParameter("messageId", messageId);
-
- OLogMessage message = query.uniqueResult();
- if (message != null) {
- session.beginTransaction();
- message.setEditedMessage(newMessage);
-
- session.merge(message);
- session.getTransaction().commit();
- }
-
- session.close();
- }
-
- public OLogMessage getMessage(String messageId) {
- Session session = getSessionFactory().openSession();
- Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class);
- query.setParameter("messageId", messageId);
-
- OLogMessage message = query.uniqueResult();
- session.close();
-
- return message;
- }
-
- public List getMessagesFromUser(String userId, String guildId, int max) {
- Session session = getSessionFactory().openSession();
- Query query = session.createNamedQuery("OLogMessage.findByUserIdEquals", OLogMessage.class);
- query.setParameter("guildId", guildId);
- query.setParameter("userId", userId);
- query.setMaxResults(max);
-
- List messages = query.getResultList();
- session.close();
-
- return messages;
- }
-
- public String getMessageString(String messageId) {
- Session session = getSessionFactory().openSession();
- Query query = session.createNamedQuery("OLogMessage.findByMessageIdEquals", OLogMessage.class);
- query.setParameter("messageId", messageId);
-
- OLogMessage message = query.uniqueResult();
- session.close();
-
- if (message == null)
- return null;
- else
- return message.getMessage();
-
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java b/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java
index 5db94c6..6a44f51 100644
--- a/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java
+++ b/src/main/java/me/bobert/netneutrality/db/entities/OBlacklist.java
@@ -10,9 +10,6 @@ import jakarta.persistence.*;
@Entity
@Table(name = "blacklist")
-@NamedQueries({
- @NamedQuery(name = "OBlacklist.findByWordEqualsIgnoreCaseAndGuild_GuildIdEquals", query = "select o from OBlacklist o where upper(o.word) = upper(:word) and o.guild.guildId = :guildId")
-})
public class OBlacklist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java b/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java
index e8b8a82..4211dc7 100644
--- a/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java
+++ b/src/main/java/me/bobert/netneutrality/db/entities/OLogMessage.java
@@ -12,10 +12,6 @@ import java.time.Instant;
@Entity
@Table(name = "log_message")
-@NamedQueries({
- @NamedQuery(name = "OLogMessage.findByMessageIdEquals", query = "select o from OLogMessage o where o.messageId = :messageId"),
- @NamedQuery(name = "OLogMessage.findByGuild_GuildIdEqualsAndUserIdEquals", query = "select o from OLogMessage o where o.guild.guildId = :guildId and o.userId = :userId")
-})
public class OLogMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OTask.java b/src/main/java/me/bobert/netneutrality/db/entities/OTask.java
new file mode 100644
index 0000000..035d3bf
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/entities/OTask.java
@@ -0,0 +1,60 @@
+package me.bobert.netneutrality.db.entities;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import jakarta.persistence.*;
+
+@Entity
+@Table(name = "tasks", uniqueConstraints = {@UniqueConstraint(columnNames = {"task_id"})})
+public class OTask {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false)
+ private Integer id;
+
+ @Column(name = "task_id", nullable = false)
+ private String taskId;
+
+ @Column(name = "task_class", nullable = false)
+ private String taskClass;
+
+ @Lob
+ @Column(name = "payload")
+ private String payload;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getTaskId() {
+ return taskId;
+ }
+
+ public void setTaskId(String taskId) {
+ this.taskId = taskId;
+ }
+
+ public String getTaskClass() {
+ return taskClass;
+ }
+
+ public void setTaskClass(String taskClass) {
+ this.taskClass = taskClass;
+ }
+
+ public String getPayload() {
+ return payload;
+ }
+
+ public void setPayload(String payload) {
+ this.payload = payload;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java b/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java
new file mode 100644
index 0000000..808bf7b
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/entities/OWebUser.java
@@ -0,0 +1,86 @@
+package me.bobert.netneutrality.db.entities;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import jakarta.persistence.*;
+import org.hibernate.annotations.ColumnDefault;
+
+import java.time.Instant;
+
+@Entity
+@Table(name = "web_users")
+public class OWebUser {
+ @Id
+ @Column(name = "id", nullable = false)
+ private String id;
+
+ @Lob
+ @Column(name = "email", nullable = false)
+ private String email;
+
+ @Column(name = "token", nullable = false)
+ private String token;
+
+ @ColumnDefault("current_timestamp()")
+ @Column(name = "expires", nullable = false)
+ private Instant expires;
+
+ @Column(name = "refresh_token", nullable = false)
+ private String refreshToken;
+
+ @Column(name = "remember_token", length = 100)
+ private String rememberToken;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public Instant getExpires() {
+ return expires;
+ }
+
+ public void setExpires(Instant expires) {
+ this.expires = expires;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public void setRefreshToken(String refreshToken) {
+ this.refreshToken = refreshToken;
+ }
+
+ public String getRememberToken() {
+ return rememberToken;
+ }
+
+ public void setRememberToken(String rememberToken) {
+ this.rememberToken = rememberToken;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java
new file mode 100644
index 0000000..b6bd763
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/repository/BlacklistRepository.java
@@ -0,0 +1,105 @@
+package me.bobert.netneutrality.db.repository;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import me.bobert.netneutrality.db.Repository;
+import me.bobert.netneutrality.db.entities.OBlacklist;
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.utils.JpaUtils;
+import net.dv8tion.jda.api.entities.Member;
+import org.hibernate.Session;
+import org.hibernate.query.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BlacklistRepository implements Repository {
+
+ @Override
+ public List findAll() {
+ List blacklists = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("FROM OBlacklist", OBlacklist.class);
+
+ blacklists.addAll(query.list());
+ });
+
+ return blacklists;
+ }
+
+ @Override
+ public OBlacklist findById(String id) {
+ return null;
+ }
+
+ @Override
+ public void deleteBy(String id) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.id = :blacklistId", OBlacklist.class);
+ query.setParameter("blacklistId", id);
+
+ OBlacklist oTask = query.uniqueResult();
+ if (oTask != null) {
+ session.remove(oTask);
+ }
+ });
+ }
+
+ public List findByGuildId(String guildId) {
+ List blacklists = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE o.guild.guildId = :guildId", OBlacklist.class);
+ query.setParameter("guildId", guildId);
+
+ blacklists.addAll(query.list());
+ });
+
+ return blacklists;
+ }
+
+ public void add(OGuild oGuild, Member member, String word) {
+ JpaUtils.runWithTransaction(session -> {
+ OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
+
+ if (bl == null) {
+ bl = new OBlacklist();
+ bl.setGuild(oGuild);
+ bl.setCreatorId(member.getId());
+ bl.setWord(word);
+ bl.setEnabled(true);
+
+ session.persist(bl);
+ } else {
+ bl.setEnabled(true);
+ session.merge(bl);
+ }
+ });
+ }
+
+ public void delete(OGuild oGuild, String word) {
+ JpaUtils.runWithTransaction(session -> {
+ OBlacklist bl = getWordFromDb(oGuild.getGuildId(), word);
+ if (bl != null) {
+ session.remove(bl);
+ }
+ });
+ }
+
+ private OBlacklist getWordFromDb(String guildId, String word) {
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ Query query = session.createQuery("SELECT o FROM OBlacklist o WHERE upper(o.word) = upper(:word) AND o.guild.guildId = :guildId", OBlacklist.class);
+ query.setParameter("guildId", guildId);
+ query.setParameter("word", word);
+ OBlacklist blacklist = query.uniqueResult();
+
+ session.close();
+ return blacklist;
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java
new file mode 100644
index 0000000..dadeb3b
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/repository/GuildRepository.java
@@ -0,0 +1,53 @@
+package me.bobert.netneutrality.db.repository;/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import me.bobert.netneutrality.db.Repository;
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.utils.JpaUtils;
+import org.hibernate.Session;
+import org.hibernate.query.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GuildRepository implements Repository {
+ @Override
+ public List findAll() {
+ List guilds = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("FROM OGuild", OGuild.class);
+
+ guilds.addAll(query.list());
+ });
+
+ return guilds;
+ }
+
+ @Override
+ public OGuild findById(String id) {
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ Query query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class);
+ query.setParameter("guildId", id);
+
+ return query.uniqueResult();
+ }
+ }
+
+ @Override
+ public void deleteBy(String id) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OGuild o WHERE o.guildId = :guildId", OGuild.class);
+ query.setParameter("guildId", id);
+
+ OGuild oGuild = query.uniqueResult();
+ if (oGuild != null) {
+ session.remove(oGuild);
+ }
+ });
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java
new file mode 100644
index 0000000..66a5a02
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/repository/GuildSettingRepository.java
@@ -0,0 +1,93 @@
+package me.bobert.netneutrality.db.repository;/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import me.bobert.netneutrality.db.GSettings;
+import me.bobert.netneutrality.db.Repository;
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.db.entities.OGuildSetting;
+import me.bobert.netneutrality.utils.JpaUtils;
+import org.hibernate.Session;
+import org.hibernate.query.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GuildSettingRepository implements Repository {
+ @Override
+ public List findAll() {
+ List settings = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("FROM OGuildSetting", OGuildSetting.class);
+
+ settings.addAll(query.list());
+ });
+
+ return settings;
+ }
+
+ @Override
+ public OGuildSetting findById(String id) {
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class);
+ query.setParameter("id", id);
+
+ return query.uniqueResult();
+ }
+ }
+
+ @Override
+ public void deleteBy(String id) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.id = :id", OGuildSetting.class);
+ query.setParameter("id", id);
+
+ OGuildSetting setting = query.uniqueResult();
+ if (setting != null) {
+ session.remove(setting);
+ }
+ });
+ }
+
+ public List findByGuild(String guildId) {
+ List settings = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("SELECT o FROM OGuildSetting o WHERE o.guild.guildId = :guildId", OGuildSetting.class);
+ query.setParameter("guildId", guildId);
+
+ settings.addAll(query.list());
+ });
+
+ return settings;
+ }
+
+ public void setSettingValue(OGuild oGuild, GSettings setting, String value) {
+ JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> {
+ OGuildSetting oGuildSetting = oGuild.getSettingByName(setting.getName());
+
+ if (value == null || setting.getDefaultValue().equals(value)) {
+ if (oGuildSetting != null) {
+ session.remove(oGuildSetting);
+ }
+ } else {
+ if (oGuildSetting == null) {
+ oGuildSetting = new OGuildSetting();
+ oGuildSetting.setGuild(oGuild);
+ oGuildSetting.setName(setting.getName());
+ oGuildSetting.setValue(value);
+
+ session.persist(oGuildSetting);
+ } else {
+ oGuildSetting.setValue(value);
+
+ session.merge(oGuildSetting);
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java
new file mode 100644
index 0000000..2294701
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/repository/MessageRepository.java
@@ -0,0 +1,106 @@
+package me.bobert.netneutrality.db.repository;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import me.bobert.netneutrality.db.Repository;
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.db.entities.OLogMessage;
+import me.bobert.netneutrality.utils.JpaUtils;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+import org.hibernate.Session;
+import org.hibernate.query.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MessageRepository implements Repository {
+ @Override
+ public List findAll() {
+ List messages = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("FROM OLogMessage", OLogMessage.class);
+
+ messages.addAll(query.list());
+ });
+
+ return messages;
+ }
+
+ @Override
+ public OLogMessage findById(String id) {
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
+ query.setParameter("messageId", id);
+
+ return query.uniqueResult();
+ }
+ }
+
+ @Override
+ public void deleteBy(String id) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
+ query.setParameter("messageId", id);
+
+ OLogMessage oLogMessage = query.uniqueResult();
+ if (oLogMessage != null) {
+ session.remove(oLogMessage);
+ }
+ });
+ }
+
+ //TODO: move to a service
+ public void log(OGuild oGuild, MessageChannel channel, Member author, Message message) {
+ JpaUtils.runWithTransaction(session -> {
+ OLogMessage logMessage = new OLogMessage();
+ logMessage.setGuild(oGuild);
+ logMessage.setMessageId(message.getId());
+ logMessage.setMessage(message.getContentRaw().trim());
+ logMessage.setChannelId(channel.getId());
+ logMessage.setUserId(author.getId());
+ logMessage.setDateSent(message.getTimeCreated().toInstant());
+
+ session.persist(logMessage);
+ });
+ }
+
+ public void logEdit(String messageId, String newMessage) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.messageId = :messageId", OLogMessage.class);
+ query.setParameter("messageId", messageId);
+
+ OLogMessage message = query.uniqueResult();
+ if (message != null) {
+ message.setEditedMessage(newMessage);
+
+ session.merge(message);
+ }
+ });
+ }
+
+ public List getMessagesFromUser(String userId, String guildId, int max) {
+ List messages = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("SELECT o FROM OLogMessage o WHERE o.guild.guildId = :guildId AND o.userId = :userId", OLogMessage.class);
+ query.setParameter("guildId", guildId);
+ query.setParameter("userId", userId);
+ query.setMaxResults(max);
+
+ messages.addAll(query.list());
+ });
+
+ return messages;
+ }
+
+ public String getMessageString(String messageId) {
+ return findById(messageId).getMessage();
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java b/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java
new file mode 100644
index 0000000..127fd0a
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/repository/TaskRepository.java
@@ -0,0 +1,56 @@
+package me.bobert.netneutrality.db.repository;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+
+import me.bobert.netneutrality.core.db.SessionManager;
+import me.bobert.netneutrality.db.Repository;
+import me.bobert.netneutrality.db.entities.OTask;
+import me.bobert.netneutrality.utils.JpaUtils;
+import org.hibernate.Session;
+import org.hibernate.query.Query;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TaskRepository implements Repository {
+
+ @Override
+ public List findAll() {
+ List tasks = new ArrayList<>();
+
+ JpaUtils.runWithSession(session -> {
+ Query query = session.createQuery("FROM OTask", OTask.class);
+
+ tasks.addAll(query.list());
+ });
+
+ return tasks;
+ }
+
+ @Override
+ public OTask findById(String id) {
+ try(Session session = SessionManager.getSessionFactory().openSession()) {
+ Query query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class);
+ query.setParameter("taskId", id);
+
+ return query.uniqueResult();
+ }
+ }
+
+ @Override
+ public void deleteBy(String id) {
+ JpaUtils.runWithTransaction(session -> {
+ Query query = session.createQuery("SELECT o FROM OTask o WHERE o.taskId = :taskId", OTask.class);
+ query.setParameter("taskId", id);
+
+ OTask oTask = query.uniqueResult();
+ if (oTask != null) {
+ session.remove(oTask);
+ }
+ });
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java b/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java
new file mode 100644
index 0000000..2a07ac4
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/service/BlacklistService.java
@@ -0,0 +1,98 @@
+package me.bobert.netneutrality.db.service;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.command.CommandExecutionResult;
+import me.bobert.netneutrality.db.entities.OBlacklist;
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.db.repository.BlacklistRepository;
+import me.bobert.netneutrality.utils.JpaUtils;
+import net.dv8tion.jda.api.entities.Member;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+public class BlacklistService {
+ private static final Logger logger = LoggerFactory.getLogger(BlacklistService.class);
+ private static final Map> blacklistCache = new ConcurrentHashMap<>();
+ private final BlacklistRepository blacklistRepository;
+ private final GuildService guildService;
+
+ public BlacklistService(GuildService guildService) {
+ this.blacklistRepository = new BlacklistRepository();
+ this.guildService = guildService;
+ init();
+ }
+
+ private void init() {
+ reloadBlacklistCache();
+ }
+
+ public void reloadBlacklistCache() {
+ OGuild oGuild = guildService.getEntity();
+ logger.debug("Reloading blacklist cache for guild: {}", oGuild.getGuildId());
+
+ JpaUtils.getWithSession(oGuild, oGuild.getGuildId(), session -> {
+ Set blacklistWords = oGuild.getBlacklists().stream()
+ .filter(OBlacklist::getEnabled)
+ .map(OBlacklist::getWord)
+ .collect(Collectors.toSet());
+
+ blacklistCache.put(oGuild.getGuildId(), blacklistWords);
+ });
+ }
+
+ public CommandExecutionResult add(Member member, String word) {
+ try {
+ blacklistRepository.add(guildService.getEntity(), member, word);
+ reloadBlacklistCache();
+
+ return CommandExecutionResult.success("Added the word [%s] to the blacklist.", word);
+ } catch (Exception e) {
+ return CommandExecutionResult.warn("Failed to add word to blacklist: " + e.getMessage());
+ }
+ }
+
+ public CommandExecutionResult delete(String word) {
+ try {
+ blacklistRepository.delete(guildService.getEntity(), word);
+ reloadBlacklistCache();
+
+ return CommandExecutionResult.success("Removed the word [%s] to the blacklist.", word);
+ } catch (Exception e) {
+ return CommandExecutionResult.warn("Failed to remove word from blacklist: " + e.getMessage());
+ }
+ }
+
+ public List getBlacklistForGuild() {
+ try {
+ return blacklistRepository.findByGuildId(guildService.getEntity().getGuildId());
+ } catch (Exception e) {
+ return List.of();
+ }
+ }
+
+ public void removeGuildBlacklist() {
+ logger.info("Clearing blacklist cache for guild: {}", guildService.getEntity().getGuildId());
+ blacklistCache.remove(guildService.getEntity().getGuildId());
+ }
+
+ public static boolean isWordBlacklisted(String guildId, String word) {
+ Set blacklistWords = blacklistCache.get(guildId);
+ return blacklistWords != null && blacklistWords.contains(word);
+ }
+
+ public static Set getBlacklist(String guildId) {
+ return blacklistCache.getOrDefault(guildId, Collections.emptySet());
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/service/GuildService.java b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java
new file mode 100644
index 0000000..cb60287
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java
@@ -0,0 +1,85 @@
+package me.bobert.netneutrality.db.service;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.db.entities.OGuild;
+import me.bobert.netneutrality.db.repository.GuildRepository;
+import me.bobert.netneutrality.utils.JpaUtils;
+import net.dv8tion.jda.api.entities.Guild;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class GuildService {
+ private static final Map services = new ConcurrentHashMap<>();
+ private static final Logger logger = LoggerFactory.getLogger(GuildService.class);
+ private final GuildRepository guildRepository = new GuildRepository();
+ private final BlacklistService blacklistService;
+ private final GuildSettingService guildSettingService;
+ private OGuild oGuild;
+
+ private GuildService(String guildId) {
+ this.oGuild = findOrCreateGuild(guildId);
+ this.blacklistService = new BlacklistService(this);
+ this.guildSettingService = new GuildSettingService(this);
+ }
+
+ public static GuildService get(String guildId) {
+ return services.computeIfAbsent(guildId, GuildService::new);
+ }
+
+ public static GuildService get(Guild guild) {
+ return get(guild.getId());
+ }
+
+ public static Map getServices() {
+ return services;
+ }
+
+ public OGuild getEntity() {
+ return oGuild;
+ }
+
+ public BlacklistService getBlacklistService() {
+ return blacklistService;
+ }
+
+ public GuildSettingService getGuildSettingService() {
+ return guildSettingService;
+ }
+
+ public OGuild findOrCreateGuild(String guildId) {
+ OGuild guildEntity = guildRepository.findById(guildId);
+ if (guildEntity == null) {
+ logger.debug("Guild[{}] doesn't exist in DB, creating entity..", guildId);
+
+ guildEntity = new OGuild();
+ guildEntity.setGuildId(guildId);
+ guildRepository.save(guildEntity);
+ }
+
+ return guildEntity;
+ }
+
+ public boolean isDebugEnabled() {
+ return oGuild.getDebug();
+ }
+
+ public void updateDebugMode(boolean debug) {
+ JpaUtils.updateWithTransaction(oGuild, () -> oGuild.setDebug(debug));
+ }
+
+ public void deleteGuild() {
+ guildRepository.deleteBy(oGuild.getGuildId());
+ }
+
+ public void reloadEntity() {
+ this.oGuild = findOrCreateGuild(oGuild.getGuildId());
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java b/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java
new file mode 100644
index 0000000..e71683f
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/service/GuildSettingService.java
@@ -0,0 +1,60 @@
+package me.bobert.netneutrality.db.service;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.db.GSettings;
+import me.bobert.netneutrality.db.entities.OGuildSetting;
+import me.bobert.netneutrality.db.repository.GuildSettingRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class GuildSettingService {
+ private static final Logger logger = LoggerFactory.getLogger(GuildSettingService.class);
+ private final GuildSettingRepository guildSettingRepository = new GuildSettingRepository();
+ private final GuildService guildService;
+ private final Map settingsCache = new ConcurrentHashMap<>();
+
+ public GuildSettingService(GuildService guildService) {
+ this.guildService = guildService;
+ reloadSettingsCache();
+ }
+
+ public Map getSettings() {
+ return settingsCache;
+ }
+
+ public String getSetting(GSettings setting) {
+ String value = settingsCache.get(setting.getName());
+
+ if (value == null) {
+ value = setting.getDefaultValue();
+ }
+
+ return value;
+ }
+
+ public void setSettingValue(GSettings setting, String value) {
+ guildSettingRepository.setSettingValue(guildService.getEntity(), setting, value);
+
+ reloadSettingsCache();
+ }
+
+ public void reloadSettingsCache() {
+ logger.debug("Reloading settings cache for guild: {}", guildService.getEntity().getGuildId());
+ settingsCache.clear();
+
+
+ List settings = guildSettingRepository.findByGuild(guildService.getEntity().getGuildId());
+ for (OGuildSetting setting : settings) {
+ settingsCache.put(setting.getName(), setting.getValue());
+ }
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java
new file mode 100644
index 0000000..5685b51
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java
@@ -0,0 +1,108 @@
+package me.bobert.netneutrality.db.service;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.tasks.PersistableTask;
+import me.bobert.netneutrality.core.tasks.TaskManager;
+import me.bobert.netneutrality.core.tasks.persistence.TaskRegistry;
+import me.bobert.netneutrality.db.entities.OTask;
+import me.bobert.netneutrality.db.repository.TaskRepository;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Objects;
+
+public class TaskService {
+ private static final Logger logger = LogManager.getLogger(TaskService.class);
+ private final TaskRepository taskRepository;
+
+ public TaskService() {
+ this.taskRepository = new TaskRepository();
+ }
+
+ public void loadTasks() {
+ TaskRegistry.loadTaskConfig();
+ logger.info("Loading persistable tasks...");
+
+ List tasks = taskRepository.findAll();
+
+ for (OTask oTask : tasks) {
+ String taskId = oTask.getTaskId();
+ String taskClass = oTask.getTaskClass();
+ logger.debug("Trying to load task with ID: {} and class: {}", taskId, taskClass);
+
+ String payload = oTask.getPayload();
+
+ try {
+ TaskRegistry.TaskCreator creator = TaskRegistry.getTaskCreator(taskClass);
+ if (creator == null) {
+ throw new IllegalArgumentException("No task creator registered for class: " + taskClass);
+ }
+
+ JSONObject json = new JSONObject(payload);
+ PersistableTask task = creator.fromPayload(json);
+ if (task == null) {
+ continue;
+ }
+
+ validateTask(task, oTask);
+ TaskManager.getInstance().insertPersistableTask(task);
+ } catch (Exception e) {
+ logger.error("Error loading task with ID {}: {}", taskId, e.getMessage());
+ }
+ }
+ }
+
+ private void validateTask(PersistableTask task, OTask oTask) throws IllegalArgumentException {
+ String taskId = oTask.getTaskId();
+ String payload = oTask.getPayload();
+
+ if (!Objects.equals(taskId, task.getId())) {
+ throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId());
+ }
+
+ JSONObject json = new JSONObject(payload);
+ String payloadId = json.getString("id");
+ if (!Objects.equals(payloadId, task.getId())) {
+ throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId());
+ }
+
+ String payloadLockId = json.getString("lockId");
+ if (!Objects.equals(payloadLockId, task.getLockId())) {
+ throw new IllegalArgumentException("Task lock ID mismatch, expected: " + task.getLockId());
+ }
+ }
+
+ public void persistTask(PersistableTask task) {
+ String taskClass = task.getClass().getName();
+ if (TaskRegistry.getTaskCreator(taskClass) == null) {
+ logger.error("No TaskCreator found for class: {}", taskClass);
+ return;
+ }
+
+ TaskRepository taskRepository = new TaskRepository();
+ OTask oTask = taskRepository.findById(task.getId());
+ if (oTask == null) {
+ oTask = new OTask();
+ oTask.setTaskId(task.getId());
+ oTask.setTaskClass(task.getClass().getName());
+ }
+
+ oTask.setPayload(task.toPayload());
+
+ logger.info("Persisting task: {}", oTask.getTaskId());
+ taskRepository.save(oTask);
+ }
+
+ public void unpersistTask(PersistableTask task) {
+ logger.debug("Unpersisting task: {}", task.getId());
+ TaskRepository taskRepository = new TaskRepository();
+
+ taskRepository.deleteBy(task.getId());
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java b/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java
deleted file mode 100644
index c006425..0000000
--- a/src/main/java/me/bobert/netneutrality/exceptions/CommandException.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package me.bobert.netneutrality.exceptions;
-
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.context.CommandContext;
-
-public abstract class CommandException extends RuntimeException {
-
- public static final String DEFAULT_MESSAGE = "Unknown error during command execution.";
- private final CommandContext context;
-
- public CommandException(String message, CommandContext context, Throwable cause) {
- super(message == null ? DEFAULT_MESSAGE : message, cause);
- this.context = context;
- }
-
- public CommandContext getCommandContext() {
- return context;
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java b/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java
index 0639059..cf190b1 100644
--- a/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java
+++ b/src/main/java/me/bobert/netneutrality/exceptions/CommandExecutionException.java
@@ -1,5 +1,4 @@
package me.bobert.netneutrality.exceptions;
-
/*
* Copyright (c) anthorob
*
@@ -8,17 +7,9 @@ package me.bobert.netneutrality.exceptions;
import me.bobert.netneutrality.core.context.CommandContext;
-/**
- * Logs the error internally, without sending a message
- */
-public class CommandExecutionException extends CommandException {
+public class CommandExecutionException extends ContextException {
- /**
- * Logs the error internally, without sending a message
- * @param context The command context
- */
public CommandExecutionException(String message, CommandContext context, Throwable cause) {
super(message, context, cause);
}
-
}
diff --git a/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java b/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java
new file mode 100644
index 0000000..d8ac1f5
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/exceptions/ContextException.java
@@ -0,0 +1,24 @@
+package me.bobert.netneutrality.exceptions;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.Context;
+
+public class ContextException extends RuntimeException {
+ public static final String DEFAULT_MESSAGE = "Unknown error during execution.";
+ private final Context context;
+
+ public ContextException(String message, Context context, Throwable cause) {
+ super(message == null ? DEFAULT_MESSAGE : message, cause);
+ this.context = context;
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+
+}
diff --git a/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java b/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java
new file mode 100644
index 0000000..0434f72
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/exceptions/GracefulContextError.java
@@ -0,0 +1,19 @@
+package me.bobert.netneutrality.exceptions;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.Context;
+
+/**
+ * When thrown, this will call {@link Context#replyEphemeral(String)} with the specified message
+ */
+public class GracefulContextError extends RuntimeException {
+
+ public GracefulContextError(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java b/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java
new file mode 100644
index 0000000..2f08e5e
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/exceptions/TaskExecutionException.java
@@ -0,0 +1,15 @@
+package me.bobert.netneutrality.exceptions;
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.core.context.TaskContext;
+
+public class TaskExecutionException extends ContextException {
+
+ public TaskExecutionException(String message, TaskContext context, Throwable cause) {
+ super(message, context, cause);
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java b/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java
new file mode 100644
index 0000000..3f5b930
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/hooks/EventsHandler.java
@@ -0,0 +1,150 @@
+package me.bobert.netneutrality.hooks;
+
+/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import me.bobert.netneutrality.context.command.MessageCommandContext;
+import me.bobert.netneutrality.context.command.MessageMenuContext;
+import me.bobert.netneutrality.context.command.SlashCommandContext;
+import me.bobert.netneutrality.context.command.UserMenuContext;
+import me.bobert.netneutrality.context.tasks.ButtonTaskContext;
+import me.bobert.netneutrality.context.tasks.EmojiTaskContext;
+import me.bobert.netneutrality.context.tasks.MessageTaskContext;
+import me.bobert.netneutrality.context.tasks.ModalTaskContext;
+import me.bobert.netneutrality.core.LogHandler;
+import me.bobert.netneutrality.core.command.CommandProcessor;
+import me.bobert.netneutrality.core.context.CommandContext;
+import me.bobert.netneutrality.core.context.Context;
+import me.bobert.netneutrality.core.context.TaskContext;
+import me.bobert.netneutrality.core.tasks.TaskManager;
+import me.bobert.netneutrality.core.thread.ThreadManager;
+import me.bobert.netneutrality.db.service.GuildService;
+import me.bobert.netneutrality.utils.Settings;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.events.Event;
+import net.dv8tion.jda.api.events.GenericEvent;
+import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
+import net.dv8tion.jda.api.events.guild.GuildLeaveEvent;
+import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
+import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
+import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
+import net.dv8tion.jda.api.hooks.EventListener;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class EventsHandler implements EventListener {
+ private static final Map, Function extends Event, Context>> listeners = new ConcurrentHashMap<>();
+ private static final Logger logger = LoggerFactory.getLogger(EventsHandler.class);
+ private final ExecutorService executorService = ThreadManager.getExecutor("events-handler");
+
+ static {
+ addListener(MessageReceivedEvent.class, event -> {
+ if (event.getAuthor().isBot()) return null;
+ if (event.isWebhookMessage()) return null;
+
+ Message message = event.getMessage();
+ String rawMessage = message.getContentDisplay().trim();
+ String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$");
+ if (rawMessage.startsWith(prefix)) {
+ return new MessageCommandContext(event);
+ }
+
+ return new MessageTaskContext(event);
+ });
+
+ addListener(UserContextInteractionEvent.class, UserMenuContext::new);
+ addListener(MessageContextInteractionEvent.class, MessageMenuContext::new);
+ addListener(SlashCommandInteractionEvent.class, SlashCommandContext::new);
+ addListener(ModalInteractionEvent.class, ModalTaskContext::new);
+ addListener(ButtonInteractionEvent.class, ButtonTaskContext::new);
+ addListener(MessageReactionAddEvent.class, EmojiTaskContext::new);
+ addGenericListener(GuildJoinEvent.class, event -> {
+ GuildService service = GuildService.get(event.getGuild());
+
+ if (service.isDebugEnabled()) {
+ LogHandler.sendMessageToDebugChannel(event.getGuild(), "Error logging enabled on this channel");
+ }
+ });
+ addGenericListener(GuildLeaveEvent.class, event -> GuildService.get(event.getGuild()).deleteGuild());
+ }
+
+ /**
+ * Register a listener for an event assocated with a {@link Context}.
+ *
+ * @param eventClass the class type of the event to listen for
+ * @param function the function to handle the event, which takes an event of the specified type and returns a {@link Context}
+ * @param the type of the event that extends the {@link Event} class
+ */
+ @SuppressWarnings("unchecked")
+ public static void addListener(Class eventClass, Function super T, Context> function) {
+ logger.info("Registering listener for {}", eventClass.getSimpleName());
+ listeners.put(eventClass, (Function extends Event, Context>) function);
+ }
+
+ /**
+ * Adds a generic listener for a specified event type. The consumer will be invoked
+ * whenever an event of the specified type is detected.
+ *
+ * @param eventClass the class type of the event to listen for
+ * @param consumer the consumer to handle the event, which takes an event of the specified type and performs an action
+ * @param the type of the event that extends the {@link Event} class
+ */
+ public static void addGenericListener(Class eventClass, Consumer consumer) {
+ addListener(eventClass, t -> {
+ consumer.accept(t);
+ return null;
+ });
+ }
+
+ @Override
+ public void onEvent(@NotNull GenericEvent event) {
+ executorService.submit(() -> handleEvent((Event) event));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void handleEvent(Event event) {
+ Class> eventClass = event.getClass();
+
+ while (eventClass != null) {
+ if (listeners.containsKey(eventClass)) {
+ Function function = (Function) listeners.get(eventClass);
+ Context context = function.apply(event);
+
+ if (context instanceof CommandContext commandContext) {
+ handleCommand(commandContext);
+ }
+
+ if (context instanceof TaskContext taskContext) {
+ handleTask(taskContext);
+ }
+
+ break;
+ }
+
+ eventClass = eventClass.getSuperclass();
+ }
+ }
+
+ private void handleCommand(CommandContext context) {
+ CommandProcessor processor = new CommandProcessor(context);
+ processor.dispatch();
+ }
+
+ private void handleTask(TaskContext context) {
+ TaskManager.getInstance().handle(context);
+ }
+}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java b/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java
deleted file mode 100644
index 1cf33e0..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/SlashCommandHooks.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package me.bobert.netneutrality.hooks;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.CommandProcessor;
-import me.bobert.netneutrality.core.context.SlashCommandContext;
-import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-public class SlashCommandHooks extends ListenerAdapter {
-
- @Override
- public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
- String commandName = event.getName();
-
- SlashCommandContext commandContext = SlashCommandContext.createContext(event);
- CommandProcessor processor = new CommandProcessor(commandName, commandContext);
- processor.dispatch();
-
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java b/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java
deleted file mode 100644
index ebdb410..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/guilds/ServerJoinHandler.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package me.bobert.netneutrality.hooks.guilds;
-
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.GuildManager;
-import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
-import net.dv8tion.jda.api.events.guild.GuildLeaveEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-public class ServerJoinHandler extends ListenerAdapter {
-
- @Override
- public void onGuildJoin(@NotNull GuildJoinEvent event) {
- GuildManager.getManager(event.getGuild());
- }
-
- @Override
- public void onGuildLeave(@NotNull GuildLeaveEvent event) {
- GuildManager.getManager(event.getGuild()).removeFromDB();
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java
deleted file mode 100644
index 3fab697..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/message/EditHandler.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package me.bobert.netneutrality.hooks.message;
-
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.core.SessionManager;
-import me.bobert.netneutrality.db.DBController;
-import me.bobert.netneutrality.db.controllers.DBMessage;
-import me.bobert.netneutrality.db.entities.OGuild;
-import me.bobert.netneutrality.utils.JpaUtils;
-import me.bobert.netneutrality.utils.MessageFunction;
-import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
-import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.hibernate.Session;
-
-import java.util.stream.Collectors;
-
-public class EditHandler extends ListenerAdapter {
-
- @Override
- public void onMessageUpdate(MessageUpdateEvent event) {
-
- if (event.getAuthor().isBot()) return;
-
- GuildManager guildManager = GuildManager.getManager(event.getGuild());
-
- if (guildManager.getGuildEntity().getLogging()) {
- OGuild guildEntity = guildManager.getGuildEntity();
-
- if (guildEntity.getLogChannel() != null && !guildEntity.getLogChannel().isEmpty()) {
- Session session = SessionManager.getSessionFactory().openSession();
- JpaUtils.tryLoad(session, guildEntity, guildEntity.getGuildId());
-
- int size = guildEntity.getLoggings()
- .stream()
- .filter(oLogging -> oLogging.getId().getChannelId().equals(event.getChannel().getId()) && oLogging.getEnabled())
- .collect(Collectors.toSet())
- .size();
-
- if (size > 0) {
- TextChannel channel = event.getGuild().getTextChannelById(guildEntity.getLogChannel());
-
- if (channel != null) {
- String originalMessage = DBController.get(DBMessage.class).getMessageString(event.getMessage().getId());
- DBController.get(DBMessage.class).logEdit(event.getMessage().getId(), event.getMessage().getContentDisplay());
-
- channel.sendMessageEmbeds(MessageFunction.buildEditMessage(event.getMember(), event.getMessage(), originalMessage)).queue();
- }
- }
- }
- }
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java
deleted file mode 100644
index 7defada..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/message/MessageReceivedHandler.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package me.bobert.netneutrality.hooks.message;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import me.bobert.netneutrality.core.CommandProcessor;
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.core.context.MessageCommandContext;
-import me.bobert.netneutrality.utils.Settings;
-import net.dv8tion.jda.api.entities.Message;
-import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-public class MessageReceivedHandler extends ListenerAdapter {
-
- @Override
- public void onMessageReceived(@NotNull MessageReceivedEvent event) {
- if (event.getAuthor().isBot()) return;
- if (event.isWebhookMessage()) return;
-
- if (!event.isFromGuild())
- return;
-
- Message message = event.getMessage();
-
- MessageCommandContext context = MessageCommandContext.createContext(event);
-
- String rawMessage = message.getContentDisplay().trim();
- String prefix = Settings.getInstance().get("NETNEUTRALITY_BOT_PREFIX", "$");
-
- if (rawMessage.startsWith(prefix)) {
- rawMessage = rawMessage.replaceFirst(String.format("^[%s\\s]{1,}", prefix), "").trim();
- // rawMessage = rawMessage.substring(prefix.length()).trim();
- String commandString = rawMessage.split(" ", 2)[0];
-
- CommandProcessor processor = new CommandProcessor(commandString, context);
- processor.dispatch();
- }
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java b/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java
deleted file mode 100644
index bcbae8c..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/message/ReactionHandler.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package me.bobert.netneutrality.hooks.message;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
-import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
-import net.dv8tion.jda.api.events.message.react.MessageReactionRemoveEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-public class ReactionHandler extends ListenerAdapter {
-
- @Override
- public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event) {
- Member member = event.getMember();
- }
-
- @Override
- public void onMessageReactionRemove(@NotNull MessageReactionRemoveEvent event) {
-
- }
-
- @Override
- public void onMessageUpdate(@NotNull MessageUpdateEvent event) {
-
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java b/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java
deleted file mode 100644
index 00543ca..0000000
--- a/src/main/java/me/bobert/netneutrality/hooks/voice/VoiceEventHandler.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package me.bobert.netneutrality.hooks.voice;
-/*
- * Copyright (c) anthorob
- *
- * https://discord.gg/qUa6zNFZsd
- */
-
-import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
-import net.dv8tion.jda.api.hooks.ListenerAdapter;
-import org.jetbrains.annotations.NotNull;
-
-public class VoiceEventHandler extends ListenerAdapter {
-
- @Override
- public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) {
- super.onGuildVoiceUpdate(event);
- }
-}
diff --git a/src/main/java/me/bobert/netneutrality/http/WebModule.java b/src/main/java/me/bobert/netneutrality/http/WebModule.java
index 03a2750..907d836 100644
--- a/src/main/java/me/bobert/netneutrality/http/WebModule.java
+++ b/src/main/java/me/bobert/netneutrality/http/WebModule.java
@@ -6,190 +6,352 @@ package me.bobert.netneutrality.http;
* https://discord.gg/qUa6zNFZsd
*/
+import com.google.gson.Gson;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
-import me.bobert.netneutrality.NetNeutrality;
-import me.bobert.netneutrality.core.LogHandler;
+import me.bobert.netneutrality.utils.Settings;
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
public class WebModule {
+ private enum Method {
+ PING, GET, POST
+ }
- private final String url;
- private final OkHttpClient client = new OkHttpClient();
- private Response response;
- //private final String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 OPR/82.0.4227.43";
- private static final String userAgent = String.format("Net Neutrality/%s (Discord bot, tony@bobert.me)", NetNeutrality.VERSION);
+ private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)";
+ private static final Logger log = LoggerFactory.getLogger(WebModule.class);
+ private static final Map cache = Collections.synchronizedMap(
+ new LinkedHashMap<>(100, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > 50; // Limit cache to 50 entries
+ }
+ }
+ );
+ private final Method method;
+ private final String url;
+ private final long cacheExpirationMillis;
+ private final OkHttpClient client;
+ private final Map headers;
+ private final Request request;
+ private CachedResponse cachedResponse;
- public WebModule(String url) {
- this.url = url;
- }
+ private WebModule(Builder builder) {
+ this.method = builder.method;
+ this.url = builder.url;
+ this.client = builder.client;
+ this.headers = builder.headers;
+ this.cacheExpirationMillis = builder.timeUnit.toMillis(builder.timeout);
+ this.request = builder.request;
+ }
- /**
- * Send a request to the specified URL in the constructor.
- *
Needs to be executed before any other method call
- *
- * @return an instance of the WebModule class with the Response of the Web Request
- */
- public WebModule request() {
- Sentry.addBreadcrumb(Breadcrumb.http(url, "get"));
- Request request = new Request.Builder().url(this.url)
- .addHeader("User-Agent", userAgent)
- .build();
- try {
- response = client.newCall(request).execute();
- Sentry.addBreadcrumb(Breadcrumb.http(url, "get", response.code()));
- } catch (IOException e) {
- Sentry.captureException(e);
- }
+ /**
+ * Invalidates a cached response for the specified URL.
+ *
+ * This method removes any cached responses matching the given URL.
+ *
+ * @param url the URL of the cached response to invalidate
+ */
+ public static void invalidateCache(String url) {
+ cache.keySet().removeIf(s -> s.contains(url));
+ }
- return this;
- }
+ /**
+ * Sends the HTTP request based on the configuration and retrieves the response.
+ *
+ * If the request is a GET request and caching is enabled, the response will be cached.
+ * Cached responses are reused until they expire.
+ *
+ * POST requests do not support caching.
+ *
+ * @return the {@code WebModule} instance, allowing further interaction with the response
+ * @throws RuntimeException if the request fails
+ */
+ public WebModule request() {
+ String cacheKey = createCacheKey(method, url, headers);
- public WebModule request(Map headers) {
- Sentry.addBreadcrumb(Breadcrumb.http(url, "get"));
- Request.Builder builder = new Request.Builder().url(this.url)
- .addHeader("User-Agent", userAgent);
+ if (method == Method.GET && cacheKey != null) {
+ CachedResponse cachedResponse = cache.get(cacheKey);
- for (Map.Entry header : headers.entrySet()) {
- builder.addHeader(header.getKey(), header.getValue());
- }
+ if (cachedResponse != null && !cachedResponse.isExpired()) {
+ log.debug("Retrieving cached response: {}", cacheKey);
+ this.cachedResponse = cachedResponse;
+ return this;
+ }
+ }
- Request request = builder.build();
+ long startTime = System.currentTimeMillis();
+ try (Response response = client.newCall(request).execute()) {
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code()));
+ String body = response.body() != null ? response.body().string() : null;
- try {
- response = client.newCall(request).execute();
- Sentry.addBreadcrumb(Breadcrumb.http(url, "get", response.code()));
- } catch (IOException e) {
- Sentry.captureException(e);
- }
+ this.cachedResponse = new CachedResponse(
+ response.code(),
+ response.headers(),
+ elapsedTime,
+ body,
+ System.currentTimeMillis() + cacheExpirationMillis,
+ null
+ );
+ } catch (IOException e) {
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ log.error("Request failed: {}", e.getMessage(), e);
+ Sentry.captureException(e);
- return this;
- }
-
- public WebModule post(TreeMap params, Map headers) {
- Sentry.addBreadcrumb(Breadcrumb.http(url, "post"));
- RequestBody body = RequestBody.create(createJson(params), MediaType.parse("application/json; charset=utf-8"));
- Request.Builder builder = new Request.Builder().url(this.url)
- .addHeader("User-Agent", userAgent);
+ this.cachedResponse = new CachedResponse(
+ 0, // Status code 0 indicates a failure
+ new Headers.Builder().build(),
+ elapsedTime,
+ null,
+ System.currentTimeMillis() + cacheExpirationMillis,
+ e.getMessage()
+ );
+ }
- for (Map.Entry header : headers.entrySet()) {
- builder.addHeader(header.getKey(), header.getValue());
- }
- Request request = builder.post(body).build();
- try {
- response = client.newCall(request).execute();
- Sentry.addBreadcrumb(Breadcrumb.http(url, "post", response.code()));
- } catch (IOException e) {
- Sentry.captureException(e);
- }
+ if (method == Method.GET && cacheKey != null) {
+ cache.put(cacheKey, this.cachedResponse);
+ }
- return this;
- }
+ return this;
+ }
- public static String createJson(TreeMap params) {
- StringBuilder json = new StringBuilder();
+ private String createCacheKey(Method method, String url, Map headers) {
+ if (method == Method.POST) {
+ return null; // no cache for post requests
+ }
- for (Map.Entry param : params.entrySet()) {
- String str = "\"" + param.getKey() + "\":\"" + param.getValue() + "\"";
- json.append(str);
- json.append(",");
- }
- json.insert(0, "{");
+ StringBuilder keyBuilder = new StringBuilder(method.name()).append(":").append(url);
+ if (headers != null) {
+ headers.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .forEach(entry -> keyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append(";"));
+ }
+ return keyBuilder.toString();
+ }
- return (json.length() > 1 ? json.substring(0, json.length() - 1) + "}" : "");
- }
+ public JSONObject getAsObject() {
+ return cachedResponse.getBody()
+ .map(JSONObject::new)
+ .orElseThrow(() -> new IllegalStateException("No body available for this response"));
+ }
- public static String createJsonObject(List key, List value) {
- if (key.size() != value.size())
- throw new IllegalArgumentException("Key list size not equal to value list size");
+ public JSONArray getAsArray() {
+ return cachedResponse.getBody()
+ .map(JSONArray::new)
+ .orElseThrow(() -> new IllegalStateException("No body available for this response"));
+ }
- StringBuilder json = new StringBuilder();
+ public String getRawData() {
+ return cachedResponse.getBody().orElse(null);
+ }
- for (int index = 0; index < key.size();) {
- json.append(String.format("\"%s\":\"%s\"", key.get(index), value.get(index)));
- json.append(",");
- }
- json.insert(0, "{");
+ public boolean isSuccess() {
+ return cachedResponse != null && cachedResponse.getStatusCode() >= 200 && cachedResponse.getStatusCode() < 400;
+ }
- return (json.length() > 1 ? json.substring(0, json.length() - 1) + "}" : "");
- }
+ public static class Builder {
+ private Request request;
+ private final String url;
+ private Method method = Method.GET;
+ private long timeout = 5;
+ private TimeUnit timeUnit = TimeUnit.MINUTES;
+ private OkHttpClient client;
+ private long connectTimeout;
+ private long readTimeout;
+ private long writeTimeout;
+ private final Map headers = new HashMap<>();
+ private final Map params = new HashMap<>();
- public static String createJsonArray(List list) {
- StringBuilder json = new StringBuilder();
+ /**
+ * Creates a new {@code Builder} instance for the specified URL.
+ *
+ * @param url the URL for the HTTP request
+ */
+ public Builder(String url) {
+ this.url = url;
+ }
- for (String str : list) {
- json.append("\"").append(str).append("\"");
- json.append(",");
- }
- json.insert(0, "[");
+ public Builder ping() {
+ this.method = Method.PING;
+ return this;
+ }
- return (json.length() > 1 ? json.substring(0, json.length() - 1) + "]" : "");
- }
+ /**
+ * Configures the request to use the HTTP GET method.
+ *
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder get() {
+ this.method = Method.GET;
+ return this;
+ }
- public static String createJsonArray(TreeMap users) {
- StringBuilder json = new StringBuilder();
+ /**
+ * Configures the request to use the HTTP POST method and sets the request parameters.
+ *
+ * @param params a map of key-value pairs to include in the POST request body
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder post(Map params) {
+ this.params.putAll(params);
+ this.method = Method.POST;
+ return this;
+ }
- for (Map.Entry param : users.entrySet()) {
- String str = "{\"" + param.getKey() + "\":\"" + param.getValue() + "\"}";
- json.append(str);
- json.append(",");
- }
- json.insert(0, "[");
+ /**
+ * Adds a custom header to the request.
+ *
+ * @param key the name of the header
+ * @param value the value of the header
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder addHeader(String key, String value) {
+ this.headers.put(key, value);
+ return this;
+ }
- return (json.length() > 1 ? json.substring(0, json.length() - 1) + "]" : "[]");
- }
+ /**
+ * Configures caching for the request.
+ *
+ * @param timeout the cache expiration duration
+ * @param timeUnit the unit of time for the cache expiration
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder cache(long timeout, TimeUnit timeUnit) {
+ this.timeout = timeout;
+ this.timeUnit = timeUnit;
+ return this;
+ }
-
- /**
- * @return the response as a JSONObject
- */
- public JSONObject getAsObject() {
- if (response == null)
- throw new RuntimeException("Request has not been sent");
+ public Builder timeout(long timeout, TimeUnit timeUnit) {
+ this.connectTimeout = timeUnit.toMillis(timeout);
+ this.readTimeout = timeUnit.toMillis(timeout);
+ this.writeTimeout = timeUnit.toMillis(timeout);
+ return this;
+ }
- try {
- ResponseBody body = response.body();
- return new JSONObject(body.string());
- } catch (IOException | NullPointerException e) {
- LogHandler.log(e);
- return new JSONObject();
- }
- }
-
- /**
- * Use if your response start with an array : [ ]
- *
- * @return the response as a JSONArray
- */
- public JSONArray getAsArray() {
- if (response == null)
- throw new RuntimeException("Request has not been sent");
+ /**
+ * Sets the connection timeout for the HTTP client.
+ *
+ * @param timeout the timeout duration
+ * @param timeUnit the unit of time for the timeout
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder connectTimeout(long timeout, TimeUnit timeUnit) {
+ this.connectTimeout = timeUnit.toMillis(timeout);
+ return this;
+ }
- try {
- ResponseBody body = response.body();
- return new JSONArray(body.string());
- } catch (IOException | NullPointerException e) {
- LogHandler.log(e);
- return new JSONArray();
- }
- }
-
- /**
- * @return the response as a String
- */
- public String getRawData() throws IOException {
- if (response == null)
- throw new RuntimeException("Request has not been sent");
+ /**
+ * Sets the read timeout for the HTTP client.
+ *
+ * @param timeout the timeout duration
+ * @param timeUnit the unit of time for the timeout
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder readTimeout(long timeout, TimeUnit timeUnit) {
+ this.readTimeout = timeUnit.toMillis(timeout);
+ return this;
+ }
- return response.body().string();
- }
+ /**
+ * Sets the write timeout for the HTTP client.
+ *
+ * @param timeout the timeout duration
+ * @param timeUnit the unit of time for the timeout
+ * @return the current {@code Builder} instance for chaining
+ */
+ public Builder writeTimeout(long timeout, TimeUnit timeUnit) {
+ this.writeTimeout = timeUnit.toMillis(timeout);
+ return this;
+ }
+ private Request createRequest() {
+ Request.Builder builder = new Request.Builder().url(url);
+ builder.addHeader("User-Agent", String.format(USER_AGENT, Settings.getVersion()));
+ headers.forEach(builder::addHeader);
+
+ if (method == Method.PING) {
+ builder.head();
+ }
+
+ if (method == Method.GET) {
+ builder.get();
+ }
+
+ if (method == Method.POST && !params.isEmpty()) {
+ RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8"));
+ builder.post(body);
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Builds and returns a configured {@link WebModule} instance.
+ *
+ * @return a new {@code WebModule} instance
+ */
+ public WebModule build() {
+ this.client = new OkHttpClient.Builder()
+ .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
+ .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
+ .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
+ .build();
+ this.request = createRequest();
+
+ return new WebModule(this);
+ }
+ }
+
+ private static class CachedResponse {
+ private final int statusCode;
+ private final Headers headers;
+ private final long elapsedTime;
+ private final String body;
+ private final long expirationTime;
+ private final String errorMessage;
+
+ public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) {
+ this.statusCode = statusCode;
+ this.headers = headers;
+ this.elapsedTime = elapsedTime;
+ this.body = body;
+ this.expirationTime = expirationTime;
+ this.errorMessage = errorMessage;
+ }
+
+ public boolean isExpired() {
+ return System.currentTimeMillis() > expirationTime;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public Headers getHeaders() {
+ return headers;
+ }
+
+ public long getElapsedTime() {
+ return elapsedTime;
+ }
+
+ public Optional getBody() {
+ return Optional.ofNullable(body);
+ }
+
+ public Optional getErrorMessage() {
+ return Optional.ofNullable(errorMessage);
+ }
+ }
}
diff --git a/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java b/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java
index a2b8f65..7637ed7 100644
--- a/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java
+++ b/src/main/java/me/bobert/netneutrality/http/server/ActionHandler.java
@@ -6,10 +6,10 @@ package me.bobert.netneutrality.http.server;
* https://discord.gg/qUa6zNFZsd
*/
+import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import me.bobert.netneutrality.NetNeutrality;
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.http.WebModule;
+import me.bobert.netneutrality.db.service.GuildService;
import net.dv8tion.jda.api.entities.Guild;
import org.apache.logging.log4j.LogManager;
import org.json.JSONObject;
@@ -26,7 +26,7 @@ public class ActionHandler extends AbstractHandler {
if (path.equals("/actions/shutdown")) {
LogManager.getLogger(NetNeutrality.class).info("Request to shutdown received");
- NetNeutrality.shutdown();
+ NetNeutrality.getInstance().shutdown();
sendStringResponse(exchange, "Shutting down..");
System.exit(0);
@@ -35,10 +35,10 @@ public class ActionHandler extends AbstractHandler {
if (path.equals("/actions/status")) {
TreeMap json = new TreeMap<>();
- json.put("health", String.valueOf(NetNeutrality.getJDA().getStatus()));
- json.put("ping", String.valueOf(NetNeutrality.getJDA().getGatewayPing()));
+ json.put("health", String.valueOf(NetNeutrality.getInstance().getJDA().getStatus()));
+ json.put("ping", String.valueOf(NetNeutrality.getInstance().getJDA().getGatewayPing()));
- sendJsonResponse(exchange, WebModule.createJson(json));
+ sendJsonResponse(exchange, new Gson().toJson(json));
return;
}
@@ -47,10 +47,10 @@ public class ActionHandler extends AbstractHandler {
String[] args = exchange.getRequestURI().toString().split("/");
String guildId = args[args.length - 1];
- Guild guild = NetNeutrality.getJDA().getGuildById(guildId);
+ Guild guild = NetNeutrality.getInstance().getJDA().getGuildById(guildId);
if (guild != null) {
- GuildManager manager = GuildManager.getManager(guild);
- Map settings = manager.getGuildConfigManager().getSettings();
+ GuildService service = GuildService.get(guild);
+ Map settings = service.getGuildSettingService().getSettings();
JSONObject setting = new JSONObject(settings);
json.put("status", "ok");
@@ -68,16 +68,11 @@ public class ActionHandler extends AbstractHandler {
String[] args = exchange.getRequestURI().toString().split("/");
String guildId = args[args.length - 1];
- Guild guild = NetNeutrality.getJDA().getGuildById(guildId);
boolean ok = true; //TODO: add bool
- if (guild == null)
- GuildManager.getManager(guildId).getGuildConfigManager().reloadSettingsCache();
- else
- GuildManager.getManager(guild).getGuildConfigManager().reloadSettingsCache();
-
+ GuildService.get(guildId).getGuildSettingService().reloadSettingsCache();
json.put("status", String.valueOf(ok));
- sendJsonResponse(exchange, WebModule.createJson(json));
+ sendJsonResponse(exchange, new Gson().toJson(json));
return;
}
diff --git a/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java b/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java
index 26016b8..c082b06 100644
--- a/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java
+++ b/src/main/java/me/bobert/netneutrality/http/server/GuildHandler.java
@@ -25,7 +25,7 @@ public class GuildHandler extends AbstractHandler {
JSONArray array = new JSONArray();
- JDA jda = NetNeutrality.getJDA();
+ JDA jda = NetNeutrality.getInstance().getJDA();
jda.getGuilds().forEach(guild -> {
JSONObject obj = new JSONObject();
@@ -45,7 +45,7 @@ public class GuildHandler extends AbstractHandler {
String guild = args[args.length - 1];
JSONArray array = new JSONArray();
- JDA jda = NetNeutrality.getJDA();
+ JDA jda = NetNeutrality.getInstance().getJDA();
if (jda.getGuildById(guild) == null) {
sendStringResponse(exchange, path);
@@ -69,7 +69,7 @@ public class GuildHandler extends AbstractHandler {
String[] args = exchange.getRequestURI().toString().split("/");
String guild = args[args.length - 1];
JSONArray array = new JSONArray();
- JDA jda = NetNeutrality.getJDA();
+ JDA jda = NetNeutrality.getInstance().getJDA();
if (jda.getGuildById(guild) == null) {
sendStringResponse(exchange, path);
diff --git a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java
index 216c7c1..c7a595d 100644
--- a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java
+++ b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java
@@ -7,7 +7,7 @@ package me.bobert.netneutrality.http.server;
*/
import com.sun.net.httpserver.HttpServer;
-import me.bobert.netneutrality.thread.BaseThreadFactory;
+import me.bobert.netneutrality.core.thread.BaseThreadFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -18,7 +18,7 @@ import java.util.concurrent.Executors;
public class WebApi {
- private final static Logger logger = LogManager.getLogger(WebApi.class);
+ private static final Logger logger = LogManager.getLogger(WebApi.class);
private HttpServer server;
diff --git a/src/main/java/me/bobert/netneutrality/music/MusicManager.java b/src/main/java/me/bobert/netneutrality/music/MusicManager.java
index 4b69cbc..3a44a69 100644
--- a/src/main/java/me/bobert/netneutrality/music/MusicManager.java
+++ b/src/main/java/me/bobert/netneutrality/music/MusicManager.java
@@ -13,46 +13,41 @@ import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
-import me.bobert.netneutrality.command.CommandExecutionResult;
-import me.bobert.netneutrality.core.GuildManager;
-import me.bobert.netneutrality.core.LogHandler;
+import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.context.CommandContext;
+import me.bobert.netneutrality.core.thread.ThreadManager;
import me.bobert.netneutrality.db.GSettings;
-import me.bobert.netneutrality.exceptions.CommandExecutionException;
+import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.music.source.UdioAudioSourceManager;
+import me.bobert.netneutrality.thread.MusicManagerCleanupThread;
import me.bobert.netneutrality.thread.MusicPlayerAutoDisconnectThread;
-import me.bobert.netneutrality.thread.ThreadManager;
+import me.bobert.netneutrality.utils.MusicUtils;
+import me.bobert.netneutrality.utils.Settings;
import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
-import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class MusicManager {
- private final static Logger logger = LogManager.getLogger(MusicManager.class);
- private final static Map managers = new ConcurrentHashMap<>();
- private final static ScheduledExecutorService executor = ThreadManager.getMusicExecutor();
-
- private final static AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
+ private static final Logger logger = LogManager.getLogger(MusicManager.class);
+ private static final Map managers = new ConcurrentHashMap<>();
+ private static final AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
private final Guild guild;
private final AudioPlayer player;
private final TrackScheduler scheduler;
- private ScheduledFuture> autoDisconnectThread;
private final int baseVolume;
private MusicManager(Guild guild) {
this.guild = guild;
this.player = playerManager.createPlayer();
this.scheduler = new TrackScheduler(this);
- this.baseVolume = Integer.parseInt(GuildManager.getManager(guild).getGuildConfigManager().getSetting(GSettings.MUSIC_VOLUME));
+ this.baseVolume = Integer.parseInt(GuildService.get(guild).getGuildSettingService().getSetting(GSettings.MUSIC_VOLUME));
guild.getAudioManager().setSendingHandler(new AudioPlayerSendHandler(player));
player.addListener(scheduler);
@@ -73,28 +68,70 @@ public class MusicManager {
playerManager.registerSourceManager(SoundCloudAudioSourceManager.createDefault());
playerManager.registerSourceManager(new UdioAudioSourceManager());
playerManager.registerSourceManager(new HttpAudioSourceManager());
+
+ String po = Settings.getInstance().get("NETNEUTRALITY_PO_TOKEN");
+ String visitor = Settings.getInstance().get("NETNEUTRALITY_VISITOR_DATA");
+ dev.lavalink.youtube.clients.Web.setPoTokenAndVisitorData(po, visitor);
+
+ ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new MusicPlayerAutoDisconnectThread(), 15, 15, TimeUnit.MINUTES);
+ ThreadManager.getScheduledExecutor().scheduleWithFixedDelay(new MusicManagerCleanupThread(), 1, 1, TimeUnit.HOURS);
}
- public boolean isNotConnectedTo(VoiceChannel vChannel) {
- return vChannel == null || !vChannel.equals(guild.getAudioManager().getConnectedChannel());
+ public static void disconnectPlayers() {
+ for (MusicManager mm : managers.values()) {
+ mm.getQueueManager().clearQueue();
+ mm.player.destroy();
+ }
}
- public synchronized void connect(VoiceChannel vChannel) {
- if (vChannel != null && isNotConnectedTo(vChannel)) {
- logger.debug("Guild {}: connecting to voice channel {}", vChannel.getGuild().getId(), vChannel.getId());
- guild.getAudioManager().openAudioConnection(vChannel);
+ public static Collection getManagers() {
+ return managers.values();
+ }
+
+ public TrackScheduler getScheduler() {
+ return scheduler;
+ }
+
+ public QueueManager getQueueManager() {
+ return scheduler.getQueueManager();
+ }
+
+ public AudioPlayer getPlayer() {
+ return player;
+ }
+
+ public Guild getGuild() {
+ return guild;
+ }
+
+ public synchronized boolean isNotConnectedTo(AudioChannel vChannel) {
+ AudioChannel current = guild.getAudioManager().getConnectedChannel();
+ return vChannel == null || current == null || !vChannel.getId().equals(current.getId());
+ }
+
+ public synchronized void connect(AudioChannel audioChannel) {
+ if (audioChannel != null) {
+ logger.debug("Guild {}: connecting to voice channel {}", audioChannel.getGuild().getId(), audioChannel.getId());
+ guild.getAudioManager().openAudioConnection(audioChannel);
guild.getAudioManager().setSelfDeafened(true);
player.setVolume(baseVolume);
-
- if (autoDisconnectThread == null)
- autoDisconnectThread = executor.scheduleWithFixedDelay(new MusicPlayerAutoDisconnectThread(this), 240, 240, TimeUnit.SECONDS);
} else {
throw new RuntimeException("Error connecting to voice channel");
}
}
- public boolean isConnected() {
- return guild != null && guild.getAudioManager().getConnectedChannel() != null;
+ public synchronized void disconnect() {
+ if (guild.getAudioManager().getConnectedChannel() != null) {
+ logger.debug("Guild {}: disconnecting from voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId());
+ player.stopTrack();
+ guild.getAudioManager().closeAudioConnection();
+ }
+
+ scheduler.detachContext();
+ }
+
+ public synchronized boolean isConnected() {
+ return guild.getAudioManager().getConnectedChannel() != null;
}
public synchronized AudioTrack getCurrentlyPlaying() {
@@ -105,81 +142,22 @@ public class MusicManager {
return player.getPlayingTrack() != null;
}
- public synchronized Member getRequesterOfCurrentTrack() {
- QueueManager.TrackInfo info = scheduler.getQueueManager().peekHeadOfQueue();
-
- if (info == null)
- return null;
-
- return info.getRequester();
+ public synchronized void play(String query, CommandContext context) {
+ play(query, context, false);
}
- protected AudioPlayer getPlayer() {
- return player;
- }
-
- public Guild getGuild() {
- return guild;
- }
-
- public synchronized void leave() {
- if (guild != null && guild.getAudioManager().getConnectedChannel() != null) {
- logger.debug("Guild {}: leaving voice channel {}", guild.getId(), guild.getAudioManager().getConnectedChannel().getId());
- player.stopTrack();
- guild.getAudioManager().closeAudioConnection();
- }
-
- scheduler.detachTextChannel();
-
- if (autoDisconnectThread != null) {
- autoDisconnectThread.cancel(true);
- autoDisconnectThread = null;
- }
- }
-
- public static void disconnectPlayers() {
- for (MusicManager mm : managers.values()) {
- mm.getScheduler().getQueueManager().clearQueue();
- mm.player.destroy();
- }
- }
-
- public synchronized void play(String query, CommandContext context, boolean needToBeConnected, boolean silent) {
+ public synchronized void play(String query, CommandContext context, boolean silent) {
logger.debug("Guild {}: request to play '{}' by user {}", guild.getId(), query, context.getGuildUser().getId());
- Member member = context.getGuildUser();
- MessageChannel channel = context.getChannel();
+ if (MusicUtils.canConnect(context)) {
+ SourceFinder.AudioSourceType type = SourceFinder.getSource(query);
+ String identifier = SourceFinder.getIdentifier(query);
+ if (identifier == null) {
+ context.reply("No source found to play the given url");
+ return;
+ }
- if (member.getVoiceState() == null) {
- logger.error("Guild {}: Voice state is null", guild.getId());
- context.replyToMessage("Voice state is null, something has gone terribly wrong.");
- LogHandler.log(new CommandExecutionException("Voice state is null", context, null));
-
- return;
- }
-
- if (!member.getVoiceState().inAudioChannel()) {
- context.replyToMessage("You are not in a voice channel");
- return;
- }
-
- VoiceChannel vChannel = (VoiceChannel) member.getVoiceState().getChannel();
- if (needToBeConnected && isNotConnectedTo(vChannel)) {
- context.replyToMessage("The player is not in your voice channel");
- return;
- }
-
- if (isNotConnectedTo(vChannel)) {
- connect(vChannel);
- scheduler.attachTextChannel(channel);
- }
-
- SourceFinder.AudioSourceType type = SourceFinder.getSource(query);
- String identifier = SourceFinder.getIdentifier(query);
-
- if (identifier != null) {
TrackLoadingHandler loadingHandler = new TrackLoadingHandler(context, this, identifier, type, silent);
-
playerManager.loadItemOrdered(this, identifier, loadingHandler);
}
}
@@ -188,30 +166,32 @@ public class MusicManager {
player.stopTrack();
}
- public synchronized CommandExecutionResult voteSkip(CommandContext context) {
- VoiceChannel voiceChannel = (VoiceChannel) guild.getAudioManager().getConnectedChannel();
+ public synchronized CommandExecutionResult resume() {
+ return getQueueManager().resume();
+ }
- if (voiceChannel == null) {
- return CommandExecutionResult.none();
+ public synchronized CommandExecutionResult voteSkip(CommandContext context) {
+ AudioChannel audioChannel = guild.getAudioManager().getConnectedChannel();
+ if (audioChannel == null) {
+ return CommandExecutionResult.warn("Bot is not connected to a voice channel");
}
if (!isCurrentlyPlaying()) {
- return CommandExecutionResult.none();
+ return CommandExecutionResult.warn("No tracks are currently playing");
}
- QueueManager.TrackInfo info = scheduler.getQueueManager().peekHeadOfQueue();
+ TrackInfo info = getQueueManager().peekHeadOfQueue();
if (info == null) {
- return CommandExecutionResult.none();
+ return CommandExecutionResult.warn("Queue is empty");
}
// Minus the bot itself
- int connected = voiceChannel.getMembers().size() - 1;
+ int connected = audioChannel.getMembers().size() - 1;
if (connected < 3) {
- context.reply("Skipping current track since not enough people are connected to the voice channel.");
- scheduler.getQueueManager().skip();
+ getQueueManager().skip();
- return CommandExecutionResult.none();
+ return CommandExecutionResult.info("Skipping current track since not enough people are connected to the voice channel.");
}
double needed = connected / 2.1;
@@ -220,12 +200,13 @@ public class MusicManager {
logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", context.getGuild().getId(), connected, needed, info.getSkips().size(), context.getGuildUser().getId());
if (info.getSkips().size() > needed) {
- return scheduler.getQueueManager().skip();
- } else {
- context.reply("Adding your vote to skip the current track. Current votes: %d/%d", info.getSkips().size(), (int) Math.ceil(needed));
+ return getQueueManager().skip();
}
- return CommandExecutionResult.none();
+ return CommandExecutionResult.success(String.format("Adding your vote to skip the current track. Current votes: %d/%d",
+ info.getSkips().size(),
+ (int) Math.ceil(needed))
+ );
}
public synchronized void setVolume(int volume) {
@@ -233,10 +214,6 @@ public class MusicManager {
}
public int getQueueSize() {
- return scheduler.getQueueManager().getQueueSize();
- }
-
- public TrackScheduler getScheduler() {
- return scheduler;
+ return getQueueManager().getQueueSize();
}
}
diff --git a/src/main/java/me/bobert/netneutrality/music/QueueManager.java b/src/main/java/me/bobert/netneutrality/music/QueueManager.java
index 9676928..acdd29c 100644
--- a/src/main/java/me/bobert/netneutrality/music/QueueManager.java
+++ b/src/main/java/me/bobert/netneutrality/music/QueueManager.java
@@ -7,7 +7,7 @@ package me.bobert.netneutrality.music;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
-import me.bobert.netneutrality.command.CommandExecutionResult;
+import me.bobert.netneutrality.core.command.CommandExecutionResult;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import org.apache.logging.log4j.LogManager;
@@ -18,68 +18,7 @@ import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Predicate;
public class QueueManager {
-
- public static class TrackInfo {
-
- private final AudioTrack track;
- private final Member member;
- private final Set skips;
- private final Logger logger = LogManager.getLogger(QueueManager.class);
-
- public TrackInfo(AudioTrack track, Member member) {
- this.track = track;
- this.member = member;
- this.skips = new HashSet<>();
- }
-
- public AudioTrack getTrack() {
- return track;
- }
-
- public Set getSkips() {
- return skips;
- }
-
- public void addSkip(Member member) {
- if (!hasSkip(member)) {
- logger.debug("VoteSkip: user={}, skip doesn't exist, before={}, after={}", member.getId(), skips.size(), skips.size() + 1);
- skips.add(member.getId());
- }
- }
-
- public boolean hasSkip(Member member) {
- return skips.contains(member.getId());
- }
-
- public Member getRequester() {
- return member;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TrackInfo trackInfo = (TrackInfo) o;
- return track.equals(trackInfo.track);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(track);
- }
-
- @Override
- public String toString() {
- return "TrackInfo{" +
- "track=" + track.getInfo().title +
- ", member=" + member.getEffectiveName() +
- ", skips=" + skips +
- '}';
- }
- }
-
private final Logger logger = LogManager.getLogger(QueueManager.class);
-
private final TrackScheduler scheduler;
private final AudioPlayer player;
private final Guild guild;
@@ -99,10 +38,6 @@ public class QueueManager {
}
}
- public TrackScheduler getScheduler() {
- return scheduler;
- }
-
public Guild getGuild() {
return guild;
}
@@ -138,7 +73,7 @@ public class QueueManager {
}
public CommandExecutionResult clearQueue() {
- scheduler.getMusicManager().leave();
+ scheduler.getMusicManager().disconnect();
if (queue.isEmpty()) {
return CommandExecutionResult.warn("Queue is empty!");
@@ -155,27 +90,33 @@ public class QueueManager {
}
}
- public void removeTracksFromUser(Member member) {
+ public CommandExecutionResult removeTracksFromUser(Member member) {
+ if (queue.stream().filter(trackInfo -> member.equals(trackInfo.getRequester())).toList().isEmpty()) {
+ return CommandExecutionResult.info(String.format("No tracks added by user %s", member.getEffectiveName()));
+ }
+
synchronized (queue) {
int size = queue.stream().filter(trackInfo -> member.equals(trackInfo.getRequester())).toList().size();
-
- if (size > 0) {
- if (size == queue.size()) {
- clearQueue();
- return;
- }
-
- TrackInfo first = queue.peek();
- queue.removeIf(trackInfo -> member.equals(trackInfo.getRequester()));
-
- if (first != null && member.equals(first.getRequester())) {
- skip(1);
- }
+ if (size == 0) {
+ return CommandExecutionResult.info(String.format("No tracks added by user %s", member.getEffectiveName()));
}
+
+ if (size == queue.size()) {
+ return clearQueue();
+ }
+
+ TrackInfo first = queue.peek();
+ queue.removeIf(trackInfo -> member.equals(trackInfo.getRequester()));
+
+ if (first != null && member.equals(first.getRequester())) {
+ skip(1);
+ }
+
+ return CommandExecutionResult.success(String.format("Removed all tracks from user %s", member.getEffectiveName()));
}
}
- public ArrayList getAllQueuedTrack() {
+ public List getAllQueuedTracks() {
synchronized (queue) {
ArrayList tracks = new ArrayList<>();
@@ -184,8 +125,15 @@ public class QueueManager {
}
}
+ public void queue(List tracks, Member member) {
+ tracks.forEach(track -> queue(track, member));
+ }
+
public void queue(AudioTrack track, Member member) {
TrackInfo info = new TrackInfo(track, member);
+ if (queue.contains(info)) {
+ return;
+ }
synchronized (queue) {
if (queue.contains(info)) {
@@ -207,6 +155,20 @@ public class QueueManager {
}
}
+ public CommandExecutionResult resume() {
+ synchronized (queue) {
+ TrackInfo next = queue.peek();
+
+ if (next != null) {
+ player.startTrack(next.getTrack().makeClone(), false);
+ return CommandExecutionResult.success("Starting playback of track " + next.getTrack().getInfo().title);
+ }
+
+ player.stopTrack();
+ return CommandExecutionResult.info("Queue is empty, leaving voice channel");
+ }
+ }
+
public void startNextTrack() {
synchronized (queue) {
TrackInfo last = queue.poll();
@@ -225,13 +187,18 @@ public class QueueManager {
}
public CommandExecutionResult skip(int nbTrack) {
- if (nbTrack == 0) {
+ if (nbTrack < 1) {
nbTrack = 1;
}
+ if (queue.isEmpty()) {
+ scheduler.getMusicManager().disconnect();
+ return CommandExecutionResult.info("Queue is empty, leaving voice channel");
+ }
+
synchronized (queue) {
if (queue.isEmpty()) {
- scheduler.getMusicManager().leave();
+ scheduler.getMusicManager().disconnect();
return CommandExecutionResult.info("Queue is empty, leaving voice channel");
}
@@ -245,7 +212,7 @@ public class QueueManager {
}
if (queue.isEmpty()) {
- scheduler.getMusicManager().leave();
+ scheduler.getMusicManager().disconnect();
return CommandExecutionResult.info("Queue is empty, leaving voice channel");
}
@@ -256,27 +223,27 @@ public class QueueManager {
}
public CommandExecutionResult shuffle() {
- if (queue.size() > 3) {
- return CommandExecutionResult.none();
+ if (queue.size() <= 3) {
+ return CommandExecutionResult.warn("The queue is too small to be shuffled");
}
synchronized (queue) {
- if (queue.size() > 3) {
- logger.debug("Guild {}: shuffling queue", guild.getId());
- TrackInfo current = queue.poll();
-
- ArrayList newQueue = new ArrayList<>(queue);
-
- Collections.shuffle(newQueue);
-
- queue.clear();
- queue.add(current);
- queue.addAll(newQueue);
-
- return CommandExecutionResult.success("Queue is now shuffled.");
+ if (queue.size() <= 3) {
+ return CommandExecutionResult.warn("The queue is too small to be shuffled");
}
- return CommandExecutionResult.none();
+ logger.debug("Guild {}: shuffling queue", guild.getId());
+ TrackInfo current = queue.poll();
+
+ ArrayList newQueue = new ArrayList<>(queue);
+
+ Collections.shuffle(newQueue);
+
+ queue.clear();
+ queue.add(current);
+ queue.addAll(newQueue);
+
+ return CommandExecutionResult.success("The queue is now shuffled.");
}
}
diff --git a/src/main/java/me/bobert/netneutrality/music/TrackInfo.java b/src/main/java/me/bobert/netneutrality/music/TrackInfo.java
new file mode 100644
index 0000000..b711421
--- /dev/null
+++ b/src/main/java/me/bobert/netneutrality/music/TrackInfo.java
@@ -0,0 +1,72 @@
+package me.bobert.netneutrality.music;/*
+ * Copyright (c) anthorob
+ *
+ * https://discord.gg/qUa6zNFZsd
+ */
+
+import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
+import net.dv8tion.jda.api.entities.Member;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class TrackInfo {
+ private final AudioTrack track;
+ private final Member member;
+ private final Set skips;
+ private static final Logger logger = LogManager.getLogger(QueueManager.class);
+
+ public TrackInfo(AudioTrack track, Member member) {
+ this.track = track;
+ this.member = member;
+ this.skips = new HashSet<>();
+ }
+
+ public AudioTrack getTrack() {
+ return track;
+ }
+
+ public Set