From 45a0a4332ecc98de5133ec498f9021d2f333e0f4 Mon Sep 17 00:00:00 2001 From: Lee Date: Fri, 27 Dec 2024 13:49:04 +0000 Subject: [PATCH] test --- .../cc/fascinated/bat/command/BatCommand.java | 140 +++++++++++++ .../fascinated/bat/command/BatSubCommand.java | 82 ++++++++ .../bat/command/CommandCategory.java | 25 +++ .../fascinated/bat/command/IBatCommand.java | 97 +++++++++ .../bat/command/annotation/BotOwner.java | 16 ++ .../bat/command/annotation/Thinking.java | 16 ++ .../bat/command/command/HelpCommand.java | 186 ++++++++++++++++++ 7 files changed, 562 insertions(+) create mode 100644 src/main/java/cc/fascinated/bat/command/BatCommand.java create mode 100644 src/main/java/cc/fascinated/bat/command/BatSubCommand.java create mode 100644 src/main/java/cc/fascinated/bat/command/CommandCategory.java create mode 100644 src/main/java/cc/fascinated/bat/command/IBatCommand.java create mode 100644 src/main/java/cc/fascinated/bat/command/annotation/BotOwner.java create mode 100644 src/main/java/cc/fascinated/bat/command/annotation/Thinking.java create mode 100644 src/main/java/cc/fascinated/bat/command/command/HelpCommand.java diff --git a/src/main/java/cc/fascinated/bat/command/BatCommand.java b/src/main/java/cc/fascinated/bat/command/BatCommand.java new file mode 100644 index 0000000..35adcaf --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/BatCommand.java @@ -0,0 +1,140 @@ +package cc.fascinated.bat.command; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +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.SubcommandGroupData; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Braydon + */ +@Setter@Getter +public abstract class BatCommand extends ListenerAdapter implements IBatCommand { + /** + * The snowflake of this command, if it + * was successfully registered on Discord. + */ + private Long snowflake; + + /** + * The data for this command. + */ + @NonNull private final SlashCommandData commandData; + + /** + * The category of this command. + */ + @NonNull private final CommandCategory category; + + /** + * The sub-commands for this command. + */ + @NonNull private final List subCommands = Collections.synchronizedList(new ArrayList<>()); + + public BatCommand(@NonNull String name, @NonNull String description, @NonNull CommandCategory category, @NonNull OptionData... options) { + this(name, description, category, DefaultMemberPermissions.ENABLED, options); + } + + public BatCommand(@NonNull String name, @NonNull String description, @NonNull CommandCategory category, @NonNull DefaultMemberPermissions defaultPermissions, @NonNull OptionData... options) { + commandData = Commands.slash(name, description) + .setDefaultPermissions(defaultPermissions) + .addOptions(options); + this.category = category; + } + + /** + * Get the name of this command. + * + * @return the command name + */ + @Override @NonNull + public String getName() { + return commandData.getName(); + } + + /** + * Get the full name of this command. + * + * @return the full name + */ + @Override @NonNull + public String getFullName() { + return getName(); + } + + /** + * Get the description of this command. + * + * @return the command description + */ + @Override @NonNull + public String getDescription() { + return commandData.getDescription(); + } + + /** + * Get the mentionable name of this command. + * + * @return the mentionable name + */ + @Override @NonNull + public final String getMentionable() { + return "".formatted(getName(), getSnowflake()); + } + + /** + * Register a sub-command to this command. + * + * @param subCommand the sub-command to register + */ + protected final void withSubCommand(@NonNull BatSubCommand subCommand) { + subCommands.add(subCommand); + + // Add the sub-command to the command data + if (subCommand.getGroupName() == null) { // No command group + getCommandData().addSubcommands(subCommand.getCommandData()); + } else { + // Add to any existing command group, otherwise create a new one + for (SubcommandGroupData subCommandGroup : getCommandData().getSubcommandGroups()) { + if (subCommand.getGroupName().equals(subCommandGroup.getName())) { + subCommandGroup.addSubcommands(subCommand.getCommandData()); + return; + } + } + getCommandData().addSubcommandGroups(new SubcommandGroupData( + subCommand.getGroupName(), + "The " + subCommand.getGroupName() + " group for /" + getName() + " " + subCommand.getName() + ).addSubcommands(subCommand.getCommandData())); + } + } + + /** + * Get a sub-command by name. + * + * @param group the sub-command group, null if none + * @param name the sub-command name + * @return the sub-command, null if not found + */ + public final BatSubCommand getSubCommand(String group, @NonNull String name) { + for (BatSubCommand subCommand : subCommands) { + String commandGroup = subCommand.getGroupName(); + + // Check if groups match (both null or both equal) + boolean groupsMatch = (group == null && commandGroup == null) || + (commandGroup != null && commandGroup.equals(group)); + if (subCommand.getName().equals(name) && groupsMatch) { + return subCommand; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/BatSubCommand.java b/src/main/java/cc/fascinated/bat/command/BatSubCommand.java new file mode 100644 index 0000000..2f5c5bb --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/BatSubCommand.java @@ -0,0 +1,82 @@ +package cc.fascinated.bat.command; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; + +/** + * A sub-command for a {@link BatCommand}. + * + * @author Braydon + */ +@Setter @Getter +public abstract class BatSubCommand implements IBatCommand { + /** + * The parent of this sub-command. + */ + @NonNull private final BatCommand parent; + + /** + * The snowflake of this sub=command, if it + * was successfully registered on Discord. + */ + private Long snowflake; + + /** + * The full name of this command, including the + * parent name, category, and sub-command name. + */ + private String fullName; + + /** + * The optional group name for this sub-command. + */ + private final String groupName; + + /** + * The data for this sub-command. + */ + @NonNull private final SubcommandData commandData; + + public BatSubCommand(@NonNull BatCommand parent, @NonNull String name, @NonNull String description, @NonNull OptionData... options) { + this(parent, null, name, description, options); + } + + public BatSubCommand(@NonNull BatCommand parent, String groupName, @NonNull String name, @NonNull String description, @NonNull OptionData... options) { + this.parent = parent; + this.groupName = groupName; + commandData = new SubcommandData(name, description).addOptions(options); + } + + /** + * Get the name of this command. + * + * @return the command name + */ + @Override @NonNull + public String getName() { + return commandData.getName(); + } + + /** + * Get the description of this command. + * + * @return the command description + */ + @Override @NonNull + public String getDescription() { + return commandData.getDescription(); + } + + /** + * Get the mentionable name of this command. + * + * @return the mentionable name + */ + @Override @NonNull + public final String getMentionable() { + return "".formatted(getFullName(), getSnowflake()); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/CommandCategory.java b/src/main/java/cc/fascinated/bat/command/CommandCategory.java new file mode 100644 index 0000000..e7561de --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/CommandCategory.java @@ -0,0 +1,25 @@ +package cc.fascinated.bat.command; + +import lombok.Getter; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.emoji.Emoji; + +/** + * The categories of {@link BatCommand}'s. + * + * @author Braydon + */ +@Getter +public enum CommandCategory { + GENERAL("⚙️"), + GUILD("🏠"); + + /** + * The emoji for this category. + */ + @NonNull private final Emoji emoji; + + CommandCategory(@NonNull String unicode) { + emoji = Emoji.fromUnicode(unicode); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/IBatCommand.java b/src/main/java/cc/fascinated/bat/command/IBatCommand.java new file mode 100644 index 0000000..c7d5112 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/IBatCommand.java @@ -0,0 +1,97 @@ +package cc.fascinated.bat.command; + +import cc.fascinated.bat.common.EnumUtils; +import cc.fascinated.bat.common.InteractionResponder; +import cc.fascinated.bat.common.model.BatGuild; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A base Rufus command. + * + * @author Braydon + */ +public interface IBatCommand { + /** + * Get the name of this command. + * + * @return the command name + */ + @NonNull String getName(); + + /** + * Get the full name of this command. + * + * @return the full name + */ + @NonNull String getFullName(); + + /** + * Get the description of this command. + * + * @return the command description + */ + @NonNull String getDescription(); + + /** + * Get the mentionable name of this command. + * + * @return the mentionable name + */ + @NonNull String getMentionable(); + + /** + * Fired when this command is executed. + *

+ * If this command has {@link BatSubCommand}'s, this + * will not be invoked for the parent, and instead only + * for the sub-commands. + *

+ * + * @param guild the ticket guild that executed this command + * @param discordGuild the Discord guild that executed this command + * @param member the member that executed this command + * @param responder the responder to the interaction + * @param event the event that triggered this command + */ + default void execute(@NonNull BatGuild guild, @NonNull Guild discordGuild, @NonNull Member member, @NonNull InteractionResponder responder, @NonNull SlashCommandInteractionEvent event) { + event.deferReply().queue(); // Thinking by default + } + + /** + * Auto complete this command. + * + * @param guild the ticket guild that executed this command + * @param discordGuild the Discord guild that executed this command + * @param member the member that executed this command + * @param event the event that triggered this auto completion + * @return the choices to auto complete + */ + @NonNull + default List autoComplete(@NonNull BatGuild guild, @NonNull Guild discordGuild, @NonNull Member member, @NonNull CommandAutoCompleteInteractionEvent event) { + return List.of(); + } + + /** + * Get the choices for an enum. + * + * @param enumClass the enum class to get the choices for + * @return the enum choices + * @param the enum type + */ + @NonNull + default > List getEnumChoices(@NonNull Class enumClass) { + return Arrays.stream(enumClass.getEnumConstants()) + .map(constant -> new Command.Choice(EnumUtils.getEnumName(constant), constant.name())) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/annotation/BotOwner.java b/src/main/java/cc/fascinated/bat/command/annotation/BotOwner.java new file mode 100644 index 0000000..7676391 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/annotation/BotOwner.java @@ -0,0 +1,16 @@ +package cc.fascinated.bat.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Commands annotated with this will only + * be executable by the bot owner. + * + * @author Braydon + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface BotOwner { } \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/annotation/Thinking.java b/src/main/java/cc/fascinated/bat/command/annotation/Thinking.java new file mode 100644 index 0000000..fd39966 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/annotation/Thinking.java @@ -0,0 +1,16 @@ +package cc.fascinated.bat.command.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Commands annotated with this will send + * "thinking..." message when executed. + * + * @author Braydon + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Thinking {} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/command/command/HelpCommand.java b/src/main/java/cc/fascinated/bat/command/command/HelpCommand.java new file mode 100644 index 0000000..a43363b --- /dev/null +++ b/src/main/java/cc/fascinated/bat/command/command/HelpCommand.java @@ -0,0 +1,186 @@ +package cc.fascinated.bat.command.command; + +import cc.fascinated.bat.common.BatEmoji; +import cc.fascinated.bat.common.Colors; +import cc.fascinated.bat.common.InteractionResponder; +import cc.fascinated.bat.common.MiscUtils; +import cc.fascinated.bat.common.model.BatGuild; +import cc.fascinated.bat.service.CommandService; +import cc.fascinated.bat.service.DiscordService; +import jakarta.annotation.Nonnull; +import lombok.NonNull; +import cc.fascinated.bat.command.CommandCategory; +import cc.fascinated.bat.command.IBatCommand; +import cc.fascinated.bat.command.BatCommand; +import cc.fascinated.bat.command.BatSubCommand; +import net.dv8tion.jda.api.EmbedBuilder; +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.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.LayoutComponent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.info.BuildProperties; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author Braydon + */ +public final class HelpCommand extends BatCommand { + private static final String CATEGORY_DROPDOWN_ID = "select-category"; + private static final String MAIN_MENU_BUTTON_ID = "main-help-menu"; + + /** + * The command service to use for the registry. + */ + @NonNull private final CommandService commandService; + + /** + * The build properties for this app, null if not available. + */ + private final BuildProperties buildProperties; + + public HelpCommand(@NonNull CommandService commandService, BuildProperties buildProperties) { + super("helpbob", "View the commands for Rufus", CommandCategory.GENERAL); + this.commandService = commandService; + this.buildProperties = buildProperties; + } + + /** + * Fired when this command is executed. + *

+ * If this command has {@link BatSubCommand}'s, this + * will not be invoked for the parent, and instead only + * for the sub-commands. + *

+ * + * @param guild the ticket guild that executed this command + * @param discordGuild the Discord guild that executed this command + * @param member the member that executed this command + * @param responder the responder to the interaction + * @param event the event that triggered this command + */ + @Override + public void execute(@NonNull BatGuild guild, @NonNull Guild discordGuild, @NonNull Member member, @NonNull InteractionResponder responder, @NonNull SlashCommandInteractionEvent event) { + responder.reply(true, buildMainMenu(member, discordGuild), buildMainMenuComponents(discordGuild).toArray(new LayoutComponent[0])).queue(); + } + + @Override + public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) { + // Switch to the selected category + if (event.getComponentId().equals(CATEGORY_DROPDOWN_ID)) { + event.editMessageEmbeds(buildCategoryEmbed(Objects.requireNonNull(event.getMember()), CommandCategory.valueOf(event.getSelectedOptions().getFirst().getValue()))) + .setComponents( + ActionRow.of(buildCategoryMenu()), + ActionRow.of(Button.primary(MAIN_MENU_BUTTON_ID, "Main Menu").withEmoji(Emoji.fromUnicode("🏠"))) + ).queue(); + } + } + + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + // Switch to the main menu + if (event.getComponentId().equals(MAIN_MENU_BUTTON_ID)) { + event.editMessageEmbeds(buildMainMenu(Objects.requireNonNull(event.getMember()), + Objects.requireNonNull(event.getGuild()))).setComponents(buildMainMenuComponents(event.getGuild())).queue(); + } + } + + /** + * Build the embed for the main menu. + * + * @param member the member to build for + * @param discordGuild the discord guild + * @return the main menu + */ + @NonNull + private MessageEmbed buildMainMenu(@NonNull Member member, @NonNull Guild discordGuild) { + String categoryNames = Arrays.stream(CommandCategory.values()) + .map(category -> "- **%s Commands**: `%s`".formatted(MiscUtils.capitalize(category.name()), commandService.getCommands(member, category).size())) + .collect(Collectors.joining("\n")); + + // Send the help menu + return new EmbedBuilder() + .setColor(Colors.DEFAULT) + .setTitle(BatEmoji.RUFUS + " Rufus Help Menu") + .setDescription(MiscUtils.arrayToString( + "Rufus is a smart and efficient Discord bot that streamlines ticket management for your server. With a focus on simplicity and responsiveness, Rufus helps your staff team handle support requests, bug reports, and general inquiries with ease.", + "", + "Here are my commands:", + categoryNames, + "", + "To get a list of commands for an individual category, select it from the dropdown below. Interested and wanna use me in your server? [Invite Me](" + MiscUtils.getInviteUrl(discordGuild.getJDA()) + ")!" + )).setFooter("Rufus v%s".formatted(buildProperties == null ? "0" : buildProperties.getVersion())).build(); + } + + /** + * Build the components for the main menu. + * + * @param guild the guild to use for the JDA instance + * @return the components + */ + @NonNull + private List buildMainMenuComponents(@NonNull Guild guild) { + return Arrays.asList( + ActionRow.of(buildCategoryMenu()), + ActionRow.of( + Button.link("https://rufus.rainnny.club", "Support Server").withEmoji(Emoji.fromUnicode("🏠")), + Button.link(MiscUtils.getInviteUrl(guild.getJDA()), "Invite Me").withEmoji(Emoji.fromUnicode("🤖")) + ) + ); + } + + /** + * Build the dropdown menu for categories. + * + * @return the category dropdown menu + */ + @NonNull + private StringSelectMenu buildCategoryMenu() { + StringSelectMenu.Builder categoryDropdown = StringSelectMenu.create(CATEGORY_DROPDOWN_ID).setPlaceholder("👉🏼 Select a category..."); + for (CommandCategory category : CommandCategory.values()) { + categoryDropdown.addOption(MiscUtils.capitalize(category.name()) + " Commands", category.name(), category.getEmoji()); + } + return categoryDropdown.build(); + } + + /** + * Build the embed for the category menu. + * + * @param member the member to build for + * @param category the category to build for + * @return the category menu + */ + @NonNull + private MessageEmbed buildCategoryEmbed(@NonNull Member member, @NonNull CommandCategory category) { + // Build the command list + StringBuilder commands = new StringBuilder(); + for (IBatCommand command : commandService.getCommands(member, category)) { + // Skip parents that have sub-commands + if (command instanceof BatCommand parentCommand && !parentCommand.getSubCommands().isEmpty()) { + continue; + } + commands.append("- ").append(command.getMentionable()).append(" - ").append(command.getDescription()).append("\n"); + } + + // Build the embed + return new EmbedBuilder() + .setColor(Colors.DEFAULT) + .setTitle(category.getEmoji().getFormatted() + " " + MiscUtils.capitalize(category.name()) + " Commands") + .setDescription(MiscUtils.arrayToString( + "Here are the commands for the " + MiscUtils.capitalize(category.name()) + " category:", + "", + commands.toString() + )).setFooter("Rufus v%s".formatted(buildProperties == null ? "0" : buildProperties.getVersion())).build(); + } +} \ No newline at end of file