diff --git a/src/main/java/me/bobert/netneutrality/command/CommandOptions.java b/src/main/java/me/bobert/netneutrality/command/CommandOptions.java new file mode 100644 index 0000000..6eb9a59 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/command/CommandOptions.java @@ -0,0 +1,129 @@ +package me.bobert.netneutrality.command; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +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; + +public class CommandOptions { + private final Command command; + private final SlashCommandData 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() { + //TODO: show available choices if option has them + StringBuilder usage = new StringBuilder(); + if (!options.isEmpty()) { + usage.append("- Usage: /").append(command.getCommandName()); + for (OptionData option : options) { + usage.append(" <").append(option.getName()).append(">"); + } + + for (OptionData option : options) { + usage.append("\n - "); + if (option.isRequired()) { + usage.append("*"); + } + usage.append(option.getName()).append(": ").append(option.getDescription()); + } + } + + 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(">"); + } + + for (OptionData option : subcommand.getOptions()) { + usage.append("\n - "); + if (option.isRequired()) { + usage.append("*"); + } + usage.append(option.getName()).append(": ").append(option.getDescription()); + } + + usage.append("\n"); + } + } + + if (!subcommandGroups.isEmpty()) { + for (SubcommandGroupData subcommandGroup : subcommandGroups) { + usage.append("- Subcommand: /").append(command.getCommandName()) + .append(" ").append(subcommandGroup.getName()) + .append(" (Description: ").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(" (Description: ").append(subcommand.getDescription()).append(")"); + + for (OptionData option : subcommand.getOptions()) { + usage.append("\n - "); + if (option.isRequired()) { + usage.append("*"); + } + usage.append(option.getName()).append(": ").append(option.getDescription()); + } + + usage.append("\n"); + + } + } + } + + if (!isEmpty()) { + usage.append("\n\n-# *: required argument"); + } + + return usage.toString(); + } +} diff --git a/src/main/java/me/bobert/netneutrality/context/MessageCommandContext.java b/src/main/java/me/bobert/netneutrality/context/MessageCommandContext.java index f59e823..c4ae060 100644 --- a/src/main/java/me/bobert/netneutrality/context/MessageCommandContext.java +++ b/src/main/java/me/bobert/netneutrality/context/MessageCommandContext.java @@ -5,18 +5,26 @@ package me.bobert.netneutrality.context; * https://discord.gg/qUa6zNFZsd */ +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.MessageCommandOption; import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.utils.Settings; -import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; 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.HashMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -35,35 +43,9 @@ public class MessageCommandContext extends CommandContext { return event; } - public String getArgument(int index) { - return getArgs().get(index); - } - - public Map getArgs() { - Map args = new HashMap<>(); - - String msg = message.getContentDisplay().trim(); - msg = msg.substring(Settings.getInstance().getPrefix().length()).trim(); - String cmd = msg.split(" ", 2)[0]; - - String[] array = msg.replaceFirst("(" + cmd + ")(\\s+)?", "").split(" "); - for (int i = 0; i < array.length; i++) { - args.put(i, array[i]); - } - - if (args.size() == 1 && args.get(0).isEmpty()) { - args.remove(0); - } - - return args; - } - - public String getMessageText() { - String msg = message.getContentDisplay().trim(); - msg = msg.substring(Settings.getInstance().getPrefix().length()).trim(); - String cmd = msg.split(" ", 2)[0]; - - return msg.replaceFirst("(" + cmd + ")( )?", ""); + @Override + public ChannelType getType() { + return event.getChannelType(); } public Message getMessage() { @@ -102,11 +84,6 @@ public class MessageCommandContext extends CommandContext { } - @Override - public void replyEphemeral(MessageCreateData messageCreateData) { - sendMessageInPrivate(messageCreateData); - } - @Override public void ack(boolean ephemeral) { @@ -114,19 +91,206 @@ public class MessageCommandContext extends CommandContext { @Override public void deleteMessage() { - if (message != null) { + if (message != null && getType() != ChannelType.PRIVATE) { message.delete().queueAfter(1, TimeUnit.SECONDS); } } - public boolean isMessageFromOwner() { - return getGuildUser().isOwner(); + @Override + public List parseOptions(String[] args, List commandOptions) { + List options = new ArrayList<>(); + Mentions mentions = message.getMentions(); + int roleMentionIndex = 0; + int userMentionIndex = 0; + int memberMentionIndex = 0; + int channelMentionIndex = 0; + int index = 0; + + for (OptionData data : commandOptions) { + OptionType type = data.getType(); + Set requiredChannelTypes = data.getChannelTypes(); + List choices = data.getChoices(); + Object value = null; + + try { + switch (type) { + case STRING -> { + //TODO: check length + String str = args[index]; + + if (str.contains("<@") && str.contains(">")) { + invalidateContext(data, "expected a string at index " + index + " but a role/user/channel was provided"); + break; + } + + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsString().equalsIgnoreCase(str)).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + } + } + + Integer min = data.getMinLength(); + Integer max = data.getMaxLength(); + + if (min != null && str.length() < min) { + invalidateContext(data, "minimum length for argument is " + min); + } + + if (max != null && str.length() > max) { + invalidateContext(data, "maximum length for argument is " + max); + } + + value = str; + } + case INTEGER -> { + long num = Long.parseLong(args[index]); + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsLong() == num).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + } + } + value = num; + + Number max = data.getMaxValue(); + Number min = data.getMinValue(); + + if (min != null && num < min.longValue()) { + value = min.longValue(); + } else if (max != null && num > max.longValue()) { + value = max.longValue(); + } + + value = num; + } + case BOOLEAN -> value = Boolean.parseBoolean(args[index]); + case NUMBER -> { + double num = Long.parseLong(args[index]); + if (!choices.isEmpty()) { + List matched = choices.stream().filter(choice -> choice.getAsDouble() == num).toList(); + if (matched.isEmpty()) { + invalidateContext(data, "not a valid choice, valid choices are: " + validChoices(choices)); + break; + } + } + + Number max = data.getMaxValue(); + Number min = data.getMinValue(); + value = num; + + if (min != null && num < min.doubleValue()) { + value = min.doubleValue(); + } else if (max != null && num > max.doubleValue()) { + value = max.doubleValue(); + } + } + case ROLE -> { + if (mentions.getRoles().size() > roleMentionIndex) { + Role role = mentions.getRoles().get(roleMentionIndex++); + if (role.getGuild().getId().equals(getGuild().getId())) { + value = role; + } + + break; + } + + invalidateContext(data, "provided argument at index " + roleMentionIndex + " is not a valid role."); + } + case USER -> { + if (mentions.getMembers().size() > memberMentionIndex) { + Member member = mentions.getMembers().get(memberMentionIndex++); + if (member.getGuild().getId().equals(getGuild().getId())) { + value = member; + break; + } + } + + if (mentions.getUsers().size() > userMentionIndex) { + value = mentions.getUsers().get(userMentionIndex++); + break; + } + + invalidateContext(data, "provided argument is not a valid user."); + } + case MENTIONABLE -> { + if (mentions.getRoles().size() > roleMentionIndex) { + Role role = mentions.getRoles().get(roleMentionIndex++); + if (role.getGuild().getId().equals(getGuild().getId())) { + value = role; + } + + break; + } + + if (mentions.getMembers().size() > memberMentionIndex) { + Member member = mentions.getMembers().get(memberMentionIndex++); + if (member.getGuild().getId().equals(getGuild().getId())) { + value = member; + } + + break; + } + + if (mentions.getUsers().size() > userMentionIndex) { + value = mentions.getUsers().get(userMentionIndex++); + break; + } + + invalidateContext(data, "provided argument is not a valid user or role."); + } + case CHANNEL -> { + //TODO: check types + if (mentions.getChannels().size() > channelMentionIndex) { + GuildChannel channel = mentions.getChannels().get(channelMentionIndex++); + + if (!requiredChannelTypes.contains(channel.getType())) { + invalidateContext(data, "not a valid channel type at index " + channelMentionIndex + + " , valid types are: " + validTypes(requiredChannelTypes)); + break; + } + + if (channel.getGuild().getId().equals(getGuild().getId())) { + value = channel; + break; + } + } + + invalidateContext(data, "provided argument is not a valid channel."); + } + } + } catch (IndexOutOfBoundsException e) { + if (data.isRequired()) { + // This should never happen because required options are validated before calling parseOptions() + throw new IllegalArgumentException("Required option " + data.getName() + " was not found at index " + index); + } + } catch (NumberFormatException ignored) { + invalidateContext(data, "the argument provided was not a number."); + } + + options.add(new MessageCommandOption(data, value)); + index++; + } + + return options; } - public boolean isMessageFromCreator() { - String creatorId = Settings.getInstance().get("NETNEUTRALITY_BOT_OWNER"); + private String validChoices(List choices) { + StringJoiner joiner = new StringJoiner(", "); + for (Command.Choice choice : choices) { + joiner.add(choice.getAsString()); + } - return creatorId.equals(getDiscordUser().getId()); + return joiner.toString(); + } + + private String validTypes(Set types) { + StringJoiner joiner = new StringJoiner(", "); + for (ChannelType type : types) { + joiner.add(type.name()); + } + + return joiner.toString(); } @Override @@ -145,11 +309,10 @@ public class MessageCommandContext extends CommandContext { } @Override - public String getSubCommand() { - if (!getArgs().isEmpty()) { - return getArgument(0); - } + public String getFullCommand() { + String rawMessage = message.getContentRaw().trim(); + String prefix = Settings.getInstance().getPrefix(); - return ""; + return rawMessage.replaceFirst(String.format("^[%s\\s]{1,}", prefix), "").trim(); } } diff --git a/src/main/java/me/bobert/netneutrality/context/SlashCommandContext.java b/src/main/java/me/bobert/netneutrality/context/SlashCommandContext.java index 3b1c2db..62fb94e 100644 --- a/src/main/java/me/bobert/netneutrality/context/SlashCommandContext.java +++ b/src/main/java/me/bobert/netneutrality/context/SlashCommandContext.java @@ -5,11 +5,18 @@ package me.bobert.netneutrality.context; * https://discord.gg/qUa6zNFZsd */ +import me.bobert.netneutrality.core.command.options.CommandOption; +import me.bobert.netneutrality.core.command.options.SlashCommandOption; import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.context.InteractionContext; +import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; import net.dv8tion.jda.api.interactions.modals.Modal; +import java.util.List; +import java.util.stream.Collectors; + public class SlashCommandContext extends CommandContext implements InteractionContext { private final SlashCommandInteractionEvent event; @@ -23,6 +30,11 @@ public class SlashCommandContext extends CommandContext implements InteractionCo return event; } + @Override + public ChannelType getType() { + return event.getChannelType(); + } + @Override public void deleteMessage() { // not implemented @@ -33,13 +45,28 @@ public class SlashCommandContext extends CommandContext implements InteractionCo event.replyModal(modal).queue(); } + @Override + public List parseOptions(String[] args, List commandOptions) { + return event.getOptions().stream().map(SlashCommandOption::new).collect(Collectors.toUnmodifiableList()); + } + @Override public String parseCommandName() { return event.getName(); } @Override - public String getSubCommand() { - return event.getFullCommandName().split(" ")[1]; + public String getFullCommand() { + return event.getFullCommandName(); + } + + @Override + public String getSubcommandGroup() { + return getFullCommand().split(" ")[2]; + } + + @Override + public String getSubcommand() { + return getFullCommand().split(" ")[1]; } } diff --git a/src/main/java/me/bobert/netneutrality/core/command/builder/CommandOptionsBuilder.java b/src/main/java/me/bobert/netneutrality/core/command/builder/CommandOptionsBuilder.java new file mode 100644 index 0000000..801b681 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/builder/CommandOptionsBuilder.java @@ -0,0 +1,24 @@ +package me.bobert.netneutrality.core.command.builder; +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import me.bobert.netneutrality.command.Command; +import me.bobert.netneutrality.command.CommandOptions; +import net.dv8tion.jda.internal.interactions.CommandDataImpl; + +public class CommandOptionsBuilder extends CommandDataImpl { + private final Command command; + + public CommandOptionsBuilder(Command command) { + super(command.getCommandName(), command.getDescription()); + this.command = command; + } + + public CommandOptions build() { + //TODO: add more command types (slash, context menu), constructors are not the same so we can't extend CommandDataImpl + return new CommandOptions(command, this); + } +}