diff --git a/README.md b/README.md index 6921421..cd7b4a6 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,62 @@ -# Bot Commons -A framework for creating bots for Discord using [JDA](https://github.com/DV8FromTheWorld/JDA) +# BotCommons +A powerful framework for creating Discord bots using [JDA (Java Discord API)](https://github.com/DV8FromTheWorld/JDA) that simplifies common bot development tasks. + +![Version](https://img.shields.io/badge/version-1.11-blue) +![Java](https://img.shields.io/badge/java-21-orange) +![License](https://img.shields.io/badge/license-MIT-green) + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Commands Framework](#commands-framework) +- [Config Framework](#config-framework) +- [Menu Framework](#menu-framework) +- [Cache Framework](#cache-framework) + +## Features + +BotCommons provides several key features to simplify Discord bot development: + +- **Commands Framework**: Easy creation and management of slash commands with parameter validation +- **Config Framework**: Simple configuration management for global and per-guild settings +- **Menu Framework**: Interactive menu system for user interactions +- **Cache Framework**: Efficient caching of Discord entities like messages, guilds, channels and members + +## Installation + +### Maven + +Add the following to your `pom.xml`: + +```xml + + dev.scyye + BotCommons + 1.11 + +``` + +### Gradle + +Add the following to your `build.gradle`: + +```groovy +dependencies { + implementation 'dev.scyye:BotCommons:1.11' +} +``` -# Features ## Commands Framework -To create a command, simply create a class that is annotated with `@MethodCommandHolder(string: Optional group)`; -Then create a function that is annotated with `@MethodCommand()`. (View the `@MethodCommand` class for more information on the parameters.) -Then, register the command using `MethodCommandManager.addCommands(CommandHolderClass.class)`. -**Below is an example of a command, along with how to use parameters:** +The Commands Framework simplifies the creation and management of slash commands. + +### Creating Commands + +1. Create a class annotated with `@CommandHolder` +2. Create methods annotated with `@Command` +3. Register the commands with the `CommandManager` ```java import botcommons.commands.*; @@ -16,88 +64,199 @@ import botcommons.commands.*; // You can also specify a group for the commands in the holder @CommandHolder public class PingCommand { - @Command(name = "ping", help = "Pong!") - public void execute(GenericCommandEvent event, - @Param( - description = "A user", - type = Param.ParamType.USER - ) - // the name of the argument is grabbed from the parameter name - User user) { - event.replySuccess("Pong! " + user.getAsMention()).finish(message -> { - // Success consumer - }); - } + @Command(name = "ping", help = "Pong!") + public void execute(GenericCommandEvent event, + @Param( + description = "A user", + type = Param.ParamType.USER + ) + // the name of the argument is grabbed from the parameter name + User user) { + event.replySuccess("Pong! " + user.getAsMention()).finish(message -> { + // Success consumer + }); + } } +``` + +### Setting Up CommandManager +```java public class Main { - // ... - public static void main(String[] args) { - JDA jda = JDABuilder.createDefault("token") - .addEventListeners(new CommandManager()) - .build(); - - CommandManager.addCommands(PingCommand.class); - } - // ... + public static void main(String[] args) { + JDA jda = JDABuilder.createDefault("token") + .addEventListeners(new CommandManager()) + .build(); + + // Initialize the CommandManager + CommandManager.init(jda); + + // Register commands + CommandManager.addCommands(PingCommand.class); + } } ``` +### Parameter Annotations + +Use the `@Param` annotation to define command parameters: + +```java +@Param( + description = "Description of the parameter", + required = true, // Default is true + type = Param.ParamType.USER, // Type of parameter (STRING, INTEGER, USER, etc.) + autocomplete = true // Enable autocomplete (requires implementing AutoCompleteHandler) +) +``` + ## Config Framework -There are 2 types of configs: `Config` and `GuildConfig`. -`Config` is a config that is shared across all servers.\ -`GuildConfig` is a config that is specific to a server. +The Config Framework provides both global and per-guild configuration management. + +### Global Config -### Config -To create a config, simply add this to your bot: ```java public static void main(String[] args) { - // ... - Config.botName = "BotName"; // This will be used as the name of the config file - Config config = Config.makeConfig(new HashMap<>(){{ - put("key", "value"); - put("another key", new String[]{"Values can be anything", "Such as lists"}); - put("a third key", new Player("PlayerName", 1000)); // Or even objects + // Set the bot name for the config file + Config.botName = "MyBot"; + + // Create a default config + Config config = Config.makeConfig(new HashMap<>() {{ + put("prefix", "!"); + put("admins", new String[]{"userId1", "userId2"}); + put("defaultSettings", new HashMap() {{ + put("welcomeMessage", true); + put("loggingEnabled", false); + }}); }}); - // ... + + // Access config values + String prefix = config.get("prefix", String.class); + boolean loggingEnabled = ((Map)config.get("defaultSettings")).get("loggingEnabled"); } ``` -Then, to get a value, simply call `config#get(String key)`. I recommend storing the `Config` instance in a variable. +### Guild Config -### GuildConfig -To create a server config, add this listener to your JDA instance, however you wish to do that.: ```java public static void main(String[] args) { - // ... - JDA jda = JDABuilder.createDefault("token") - .addEventListeners(new ConfigManager(new HashMap<>(){{ - put("key", "value"); - put("another key", new String[]{"Values can be anything", "Such as lists"}); - put("a third key", new Player("PlayerName", 1000)); // Or even objects - }})) - .build(); - // ... + JDA jda = JDABuilder.createDefault("token") + .addEventListeners(new ConfigManager(new HashMap<>() {{ + put("prefix", "!"); + put("welcomeMessage", true); + put("loggingEnabled", false); + }})) + .build(); + + // Later, get a guild's config + Config guildConfig = event.getConfig(); + boolean welcomeMessage = guildConfig.get("welcomeMessage", Boolean.class); + + // Update a value + ConfigManager.getInstance().setValue(guildId, "welcomeMessage", false); } ``` -### ***__NOTE:__ This will automatically create the `/sql` and `/config` commands*** +**Note:** This will automatically create the `/sql` and `/config` commands. +## Menu Framework -# Disclaimer -EVERYTHING AFTER THIS POINT IS NOT ACCURATE. I WILL UPDATE IT SOON. +The Menu Framework allows you to create interactive menus with buttons and pagination. +### Setting Up MenuManager -## Menu Framework -### ***__NOTE:__ YOU MUST call `JDA.addEventListener(PaginationListener)` to enable the menu framework.*** +```java +public static void main(String[] args) { + JDA jda = JDABuilder.createDefault("token") + .build(); + + // Create menu manager instance + MenuManager menuManager = new MenuManager(jda); +} +``` -To create a menu, simply call `PaginatedMenuHandler#addMenu(PaginatedMenuHandler#buildMenu())`, and pass in the data for each page. +### Creating and Registering Menus -You can also reply to commands with a menu, by calling `CommandEvent#replyMenu(PaginatedMenuHandler#buildMenu())`. +```java +// Create a simple menu implementation +IMenu helpMenu = new IMenu() { + @Override + public String getMenuId() { + return "help_menu"; + } + + @Override + public MessageCreateAction generateMenu(Object... args) { + return new MessageCreateAction() + .setContent("Help Menu") + .addActionRow( + Button.primary("prev", "Previous"), + Button.primary("next", "Next") + ); + } + + @Override + public void handleButtonInteraction(ButtonInteractionEvent event) { + String buttonId = event.getComponentId(); + if (buttonId.equals("prev")) { + // Handle previous button + } else if (buttonId.equals("next")) { + // Handle next button + } + } +}; +// Register the menu +menuManager.registerMenu(helpMenu); -### ***__NOTE:__ YOU MUST call `JDA.addEventListener(PaginationListener)` to enable the menu framework.*** +// Send the menu to a channel +menuManager.sendMenu("help_menu", channelId); + +// Or reply to a command with a menu +event.replyMenu("help_menu").finish(); +``` ## Cache Framework -### TODO + +The Cache Framework allows efficient caching of Discord entities for improved performance. + +### Initializing Cache + +```java +public static void main(String[] args) { + JDA jda = JDABuilder.createDefault("token") + .build(); + + // Initialize cache with various options + CacheManager.init( + jda, + true, // Cache guild members + true, // Cache mutual guilds + true, // Cache channel messages + true, // Cache user messages + true // Cache users + ); +} +``` + +### Accessing Cached Data + +```java +// Access cached members for a guild +List members = CacheManager.guildMemberCache.get(guildId); + +// Access cached messages +List messages = CacheManager.channelMessageCache.get(channelId); + +// Update the cache to JSON files +CacheManager.update(); +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/src/main/java/botcommons/cache/CacheManager.java b/src/main/java/botcommons/cache/CacheManager.java index f93dd75..4557c37 100644 --- a/src/main/java/botcommons/cache/CacheManager.java +++ b/src/main/java/botcommons/cache/CacheManager.java @@ -20,6 +20,9 @@ import java.util.HashMap; import java.util.List; +/** + * CacheManager is a utility class to manage various caches for JDA entities. + */ @SuppressWarnings("unused") public class CacheManager extends ListenerAdapter { private CacheManager() {} @@ -30,8 +33,16 @@ private CacheManager() {} public static final HashMap> userMessageCache = new HashMap<>(); public static final HashMap userCache = new HashMap<>(); - public static void init(JDA jda, boolean guildMembers, boolean mutualGuilds, boolean channelMessages, boolean userMessages, boolean users) { - if (jda == null) throw new IllegalArgumentException("JDA instance cannot be null"); + /** + * Initializes the CacheManager with the provided JDA instance and cache options. + * @param jda The JDA instance to fetch data from. This cannot be null. + * @param guildMembers Whether to enable the guild member cache. This will load all members in each guild when they are ready. + * @param mutualGuilds Whether to enable the mutual guilds cache. This will store all guilds a user is in. + * @param channelMessages Whether to enable the channel messages cache. This will store all messages sent in guild channels. + * @param userMessages Whether to enable the user messages cache. This will store all messages sent by users in guild channels. + * @param users Whether to enable the user cache. This will store all users that are in guilds or have sent messages in guild channels. + */ + public static void init(@NotNull JDA jda, boolean guildMembers, boolean mutualGuilds, boolean channelMessages, boolean userMessages, boolean users) { if (!guildMembers && !mutualGuilds && !channelMessages && !userMessages && !users) throw new IllegalArgumentException("At least one cache must be enabled"); jda.addEventListener(new CacheManager()); @@ -44,6 +55,9 @@ public static void init(JDA jda, boolean guildMembers, boolean mutualGuilds, boo if (users) JsonUtils.createCache(userCache, "user_cache"); } + /** + * Updates all caches to their respective JSON files. + */ public static void update() { JsonUtils.updateCache(guildMemberCache, "guild_member_cache"); JsonUtils.updateCache(mutualGuildsCache, "mutual_guilds_cache"); @@ -53,7 +67,6 @@ public static void update() { JsonUtils.updateCache(userCache, "user_cache"); } - // Message cache code @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { if (!event.isFromGuild()) @@ -109,24 +122,51 @@ public void onGuildMemberRemove(@NotNull GuildMemberRemoveEvent event) { update(); } + /** + * Represents a message structure that holds information about a message in a guild. + * @param id The unique identifier of the message. + * @param content The raw content of the message. + * @param contentDisplay The content of the message as it would be displayed, which may differ from the raw content due to formatting or mentions. + * @param channel The channel structure where the message was sent. This provides context about the channel in which the message resides. + * @param author The author of the message, represented as a MemberStructure. This can be null if the message was sent by a bot or if the author information is not available. + */ public record MessageStructure(String id, String content, String contentDisplay, ChannelStructure channel, @Nullable MemberStructure author) { static MessageStructure fromMessage(Message message) { return new MessageStructure(message.getId(), message.getContentRaw(), message.getContentDisplay(), ChannelStructure.fromChannel(message.getGuildChannel()), MemberStructure.fromMember(message.getMember())); } } + /** + * Represents a structure for a guild, containing information about its channels, member count, name, and ID. + * @param channels A list of ChannelStructure objects representing the channels in the guild. This provides an overview of the communication channels available within the guild. + * @param memberCount The total number of members in the guild. This gives an indication of the size of the guild and its community. + * @param name The name of the guild. This is a human-readable identifier for the guild, making it easier to recognize and refer to. + * @param id The unique identifier of the guild. This is a string that serves as a primary key for identifying the guild within the Discord API and is essential for performing operations related to this specific guild. + */ public record GuildStructure(List channels, int memberCount, String name, String id) { static GuildStructure fromGuild(Guild guild) { return new GuildStructure(guild.getChannels().stream().map(ChannelStructure::fromChannel).toList(), guild.getMemberCount(), guild.getName(), guild.getId()); } } + /** + * Represents a structure for a channel within a guild, encapsulating its unique identifier, the guild it belongs to, and its name. + * @param id The unique identifier of the channel. This serves as a primary key for identifying the channel within the Discord API, allowing for precise operations related to this specific channel. + * @param guild The unique identifier of the guild to which this channel belongs. This provides context about the guild structure and allows for operations that are specific to the guild. + * @param name The name of the channel. This is a human-readable identifier for the channel, making it easier to recognize and refer to. It can be used in user interfaces or logs to display the channel's name instead of its ID. + */ public record ChannelStructure(String id, String guild, String name) { static ChannelStructure fromChannel(GuildChannel channel) { return new ChannelStructure(channel.getId(), channel.getGuild().getId(), channel.getName()); } } + /** + * Represents a structure for a member within a guild, encapsulating their unique identifier, the guild they belong to, and their name. + * @param id The unique identifier of the member. This serves as a primary key for identifying the member within the Discord API, allowing for precise operations related to this specific member. + * @param guild The {@link GuildStructure} representing the guild to which this member belongs. This provides context about the guild structure and allows for operations that are specific to the guild. + * @param name The name of the member as it would be displayed in Discord. This is a human-readable identifier for the member, making it easier to recognize and refer to them. It can be used in user interfaces or logs to display the member's name instead of their ID. + */ public record MemberStructure(String id, GuildStructure guild, String name) { static MemberStructure fromMember(Member member) { return new MemberStructure(member.getId(), GuildStructure.fromGuild(member.getGuild()), member.getEffectiveName()); diff --git a/src/main/java/botcommons/commands/AutoCompleteHandler.java b/src/main/java/botcommons/commands/AutoCompleteHandler.java index 7b88119..a3c50b2 100644 --- a/src/main/java/botcommons/commands/AutoCompleteHandler.java +++ b/src/main/java/botcommons/commands/AutoCompleteHandler.java @@ -5,5 +5,9 @@ @Retention(RetentionPolicy.RUNTIME) public @interface AutoCompleteHandler { + /** + * The name of the command that this auto complete handler is for. + * @return value + */ String[] value(); } diff --git a/src/main/java/botcommons/commands/Command.java b/src/main/java/botcommons/commands/Command.java index 541d0fb..b005acd 100644 --- a/src/main/java/botcommons/commands/Command.java +++ b/src/main/java/botcommons/commands/Command.java @@ -8,6 +8,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * This annotation is used to define a command for the bot. + *

+ * The name, help, aliases, usage, category and permission can be specified. + *

+ * The userContext defines where this command can be used (e.g. GUILD, PRIVATE_CHANNEL) + */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Command { diff --git a/src/main/java/botcommons/commands/CommandHolder.java b/src/main/java/botcommons/commands/CommandHolder.java index 42fdf71..504140c 100644 --- a/src/main/java/botcommons/commands/CommandHolder.java +++ b/src/main/java/botcommons/commands/CommandHolder.java @@ -2,7 +2,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; - +/** + * This annotation is used to define a command holder class. + */ @Retention(RetentionPolicy.RUNTIME) public @interface CommandHolder { String group() default "N/A"; diff --git a/src/main/java/botcommons/commands/CommandInfo.java b/src/main/java/botcommons/commands/CommandInfo.java index c63b369..040c21c 100644 --- a/src/main/java/botcommons/commands/CommandInfo.java +++ b/src/main/java/botcommons/commands/CommandInfo.java @@ -16,17 +16,27 @@ import java.util.List; import java.util.function.Predicate; +/** + * This class holds information about a command. + */ @SuppressWarnings("unused") public class CommandInfo { private static final HashMap, OptionType> optionTypeParams = new HashMap<>() {{ put(String.class, OptionType.STRING); put(Integer.class, OptionType.INTEGER); + put(int.class, OptionType.INTEGER); put(Boolean.class, OptionType.BOOLEAN); + put(boolean.class, OptionType.BOOLEAN); put(Long.class, OptionType.INTEGER); + put(long.class, OptionType.INTEGER); put(Double.class, OptionType.NUMBER); + put(double.class, OptionType.NUMBER); put(Float.class, OptionType.NUMBER); + put(float.class, OptionType.NUMBER); put(Short.class, OptionType.INTEGER); + put(short.class, OptionType.INTEGER); put(Byte.class, OptionType.INTEGER); + put(byte.class, OptionType.INTEGER); put(Character.class, OptionType.STRING); put(List.class, OptionType.STRING); put(ArrayList.class, OptionType.STRING); @@ -51,10 +61,20 @@ public class CommandInfo { public String usage = ""; public Method method; + /** + * Get the option with the specified name. + * @param name the name of the option to retrieve + * @return the {@link CommandInfo.Option} with the specified name, or null if no such option exists + */ public CommandInfo.Option getOption(String name) { return Arrays.stream(args).filter(option -> option.name.equals(name)).findFirst().orElse(null); } + /** + * Creates a CommandInfo instance from a method with the {@link Command} annotation. + * @param command the method to create the {@link CommandInfo} from. This method must have the {@link Command} annotation. + * @return a {@link CommandInfo} instance containing the information from the method. + */ public static CommandInfo from(@Nullable Method command) { if (command == null) return null; @@ -96,6 +116,11 @@ public static CommandInfo from(@Nullable Method command) { return info; } + /** + * Creates a CommandInfo instance from a {@link GenericCommandEvent}. + * @param event the {@link GenericCommandEvent} to create the {@link CommandInfo} from. This event should be associated with a command. + * @return a {@link CommandInfo} instance containing the information from the event. + */ public static CommandInfo from(GenericCommandEvent event) { boolean sub = event.getSubcommandName() != null || event.getSubcommandGroup() != null; String command = sub ? event.getCommandName() + (event.getSubcommandGroup()!=null? " " + event.getSubcommandGroup() + " ": " ") + @@ -104,6 +129,9 @@ public static CommandInfo from(GenericCommandEvent event) { return from(CommandManager.getCommand(command)); } + /** + * Represents an option (argument) for a command. + */ @Getter public static class Option { private String name; diff --git a/src/main/java/botcommons/commands/CommandManager.java b/src/main/java/botcommons/commands/CommandManager.java index c949c51..20e3a5a 100644 --- a/src/main/java/botcommons/commands/CommandManager.java +++ b/src/main/java/botcommons/commands/CommandManager.java @@ -25,6 +25,11 @@ import java.util.*; import java.util.function.Function; +/** + * This class manages the commands for the bot. + *

+ * Call {@link #init(JDA)} to initialize the command manager. + */ public class CommandManager extends ListenerAdapter { private static final HashMap commands = new HashMap<>(); private static final HashMap>> subcommands = new HashMap<>(); @@ -34,15 +39,28 @@ private CommandManager() {} private static Function commandRunCheck = ($) -> true; + /** + * This method initializes the command manager with a JDA instance and a command run check function. + * @param jda The JDA instance to register the commands with. + * @param commandRunCheck A function that takes a {@link GenericCommandEvent} and returns a boolean indicating whether the command should be executed or not. + */ public static void init(JDA jda, Function commandRunCheck) { init(jda); CommandManager.commandRunCheck = commandRunCheck; } + /** + * This method initializes the command manager with a JDA instance. + * @param jda The JDA instance to register the commands with. + */ public static void init(JDA jda) { jda.addEventListener(new CommandManager()); } + /** + * This method adds command holders to the command manager. + * @param holders The classes that hold the commands. Each class should be annotated with {@link CommandHolder}. + */ public static void addCommands(Class... holders) { for (var holder : holders) { CommandHolder meta = holder.getAnnotation(CommandHolder.class); @@ -70,6 +88,10 @@ public static void addCommands(Class... holders) { } } + /** + * This method adds subcommands to the command manager. + * @param holder The class that holds the subcommands. This class should be annotated with {@link CommandHolder}. + */ private static void addSubcommands(Class holder) { CommandHolder meta = holder.getAnnotation(CommandHolder.class); if (meta == null) { @@ -244,6 +266,11 @@ private static boolean checks(CommandInfo info, GenericCommandEvent event, Metho put(OptionType.UNKNOWN, Object.class); }}; + /** + * This method retrieves the command method associated with a given command string. + * @param command The command string to look up. This can be the name of the command or an alias. + * @return The method associated with the command, or null if no matching command is found. + */ public static Method getCommand(String command) { // First, check if the command is a direct match or an alias Method possibleCommand = commands.entrySet().stream() diff --git a/src/main/java/botcommons/commands/GenericCommandEvent.java b/src/main/java/botcommons/commands/GenericCommandEvent.java index 672906d..6bd7081 100644 --- a/src/main/java/botcommons/commands/GenericCommandEvent.java +++ b/src/main/java/botcommons/commands/GenericCommandEvent.java @@ -24,7 +24,11 @@ import java.util.function.Function; import java.util.function.Predicate; -// Ignore possible null issues +/** + * This class represents a generic command event for slash commands. + * It wraps the {@link SlashCommandInteractionEvent} and provides additional utility methods + * for handling command arguments and replies. + */ @SuppressWarnings({"ConstantConditions", "unused"}) public class GenericCommandEvent { private final SlashCommandInteractionEvent slashCommandEvent; @@ -35,70 +39,126 @@ private GenericCommandEvent(SlashCommandInteractionEvent slashCommandEvent) { this.slashCommandEvent = slashCommandEvent; } + /** + * Creates a new instance of {@link GenericCommandEvent} from a {@link SlashCommandInteractionEvent}. + * @param event The {@link SlashCommandInteractionEvent} to wrap. + * @return A new instance of {@link GenericCommandEvent} that wraps the provided {@link SlashCommandInteractionEvent}. + */ public static GenericCommandEvent of(@NotNull SlashCommandInteractionEvent event) { GenericCommandEvent e = new GenericCommandEvent(event); e.replyContext = new ReplyContext(event); return e; } + /** + * @return Returns the {@link JDA} instance associated with this command event. + */ public JDA getJDA() { return slashCommandEvent.getJDA(); } + /** + * @return Returns true if the command was invoked from a guild (server), false otherwise. + */ public boolean isGuild() { return slashCommandEvent.isFromGuild(); } + /** + * @return Returns true if the command was invoked in an area the bot can't access + */ public boolean isDetached() { return slashCommandEvent.getChannel().isDetached(); } + /** + * @return Returns the {@link Guild} associated with this command event, or null if the command was invoked in a private channel (DM). + */ @Nullable public Guild getGuild() { return slashCommandEvent.getGuild(); } + /** + * @return Returns the ID of the guild associated with this command event. + * If the command was invoked in a private channel (DM), it returns "-1". + */ public String getGuildId() { return isGuild()? getGuild().getId():"-1"; } + /** + * @return Returns the user who invoked the command. + */ public User getUser() { return slashCommandEvent.getUser(); } + /** + * @return Returns the ID of the user who invoked the command. + */ public String getUserId() { return getUser().getId(); } + /** + * @return Returns the member who invoked the command in a guild context, or null if the command was invoked in a private channel (DM). + */ @Nullable public Member getMember() { return slashCommandEvent.getMember(); } + /** + * @return The {@link SlashCommandInteraction} associated with this command event. + */ public SlashCommandInteraction getSlashCommandInteraction() { return slashCommandEvent.getInteraction(); } + /** + * @return Returns the {@link MessageChannel} where the command was invoked. + */ public MessageChannel getChannel() { return slashCommandEvent.getChannel(); } + /** + * @return Returns the name of the command that was invoked. + * This is the name of the slash command as defined when it was registered. + */ public String getCommandName() { return slashCommandEvent.getName(); } + /** + * @return Returns the name of the subcommand that was invoked, if applicable. + */ public String getSubcommandName() { return slashCommandEvent.getSubcommandName(); } + /** + * @return Returns the name of the subcommand group that was invoked, if applicable. + * This is useful for commands that have subcommand groups. + */ public String getSubcommandGroup() { return slashCommandEvent.getSubcommandGroup(); } + /** + * @deprecated Useless, text commands are no longer supported. + * @return Returns true if this command is a slash command. (always) + */ + @Deprecated(forRemoval = true) boolean isSlashCommand() { return true; } + /** + * This method retrieves the arguments passed to the command in a structured way. + * @return An array of {@link Data} objects representing the options passed to the command. + */ public Data[] getArgs() { return Arrays.stream(slashCommandEvent.getOptions().toArray(OptionMapping[]::new)).map( optionMapping -> { @@ -124,6 +184,13 @@ public record Data(CommandInfo.Option option, Object value) { } + /** + * This method retrieves the value of a specific argument by its name and casts it to the specified type. + * @param name The name of the argument to retrieve. This should match the name of the option as defined in the command registration. + * @param type The class type to which the argument value should be cast. This allows for type-safe retrieval of the argument value. + * @return The value of the argument cast to the specified type, or null if the argument is not present or cannot be cast to the specified type. + * @param The type of the argument to retrieve. This is a generic type parameter that allows the method to return the value in the desired type. + */ public T getArg(String name, Class type) { CommandInfo from = CommandInfo.from(this); CommandInfo.Option option = from.getOption(name); @@ -147,6 +214,9 @@ public T getArg(String name, Class type) { return null; } + /** + * @return Returns the {@link botcommons.config.ConfigManager.Config} associated with the current guild. + */ public ConfigManager.Config getConfig() { return ConfigManager.getInstance().getConfigs().get(getGuildId()); } @@ -194,6 +264,13 @@ public ReplyContext replyMenu(String menuId, Object... args) { return this.replyContext.menu(menuId, args); } + /** + * This method registers a menu with a specific ID and then returns the reply context for that menu. + * @param id The ID to register the menu with. This should be unique to avoid conflicts with other menus. + * @param menu The {@link BaseMenu} instance to register. This represents the menu that will be displayed to the user when they interact with the command. + * @param args Additional arguments to pass to the menu when it is invoked. This allows for dynamic content to be passed into the menu, such as user-specific data or command context. + * @return Returns the {@link ReplyContext} associated with the registered menu. This allows for further customization of the reply context, such as adding more options or handling user interactions with the menu. + */ public ReplyContext replyMenu(String id, BaseMenu menu, Object... args) { MenuManager.registerMenuWithId(id+"-fake", menu); System.out.println("Registered menu with id: " + id); diff --git a/src/main/java/botcommons/commands/Param.java b/src/main/java/botcommons/commands/Param.java index 2bf6f73..c4414fc 100644 --- a/src/main/java/botcommons/commands/Param.java +++ b/src/main/java/botcommons/commands/Param.java @@ -5,11 +5,34 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * This annotation is used to define a parameter for a command method. + */ @Retention(RetentionPolicy.RUNTIME) public @interface Param { + /** + * The name of the parameter. + */ String description(); + + /** + * Whether this parameter is required or optional. + */ boolean required() default true; + + /** + * An array of choices for the parameter. This is used to provide a list of valid options for the parameter in the command interface. + */ String[] choices() default {}; + + /** + * Whether to enable autocomplete for this parameter. If set to true, the bot will provide suggestions for this parameter based on user input. + */ boolean autocomplete() default false; + + /** + * The {@link OptionType} of the parameter. This defines the data type of the parameter in the command interface (e.g. STRING, INTEGER, BOOLEAN, etc.). + * Default is STRING. + */ OptionType type() default OptionType.STRING; } diff --git a/src/main/java/botcommons/commands/ReplyContext.java b/src/main/java/botcommons/commands/ReplyContext.java index 74aa8f7..248f818 100644 --- a/src/main/java/botcommons/commands/ReplyContext.java +++ b/src/main/java/botcommons/commands/ReplyContext.java @@ -77,6 +77,14 @@ public ReplyContext menu(String menuId, Object... args) { return this; } + /** + * Adds a listener for a specific event type that will only be called once. + * @param eventType The type of event to listen for. This should be a subclass of {@link GenericEvent}. + * @param filter A predicate to filter the events. This allows you to specify conditions for the events that should trigger the listener. + * @param listener The listener function that will be called when the event is received. This should be a function that takes an instance of the event type and returns Void. + * @return The current instance of {@link ReplyContext} for chaining. + * @param The type of the event to listen for. This should be a subclass of {@link GenericEvent}. + */ public ReplyContext listenOnce(Class eventType, Predicate filter, Function listener) { once = new OnceListener<>(eventType, interactionEvent.getJDA(), filter, listener); return this; @@ -95,6 +103,11 @@ private void markAsFinished() { this.once = null; } + /** + * This method finalizes the reply context and sends the message to Discord. It will handle all the cases for sending a message, including: + * @param consumer A consumer that will be called with the resulting message after it has been sent. This allows you to perform additional actions on the message, such as logging or further processing. + * @return true if the reply was successfully sent, false otherwise. This method will also mark the reply context as finished. + */ public boolean finish(Consumer consumer) { if (finished) throw new IllegalStateException("ReplyContext already finished"); diff --git a/src/main/java/botcommons/config/Config.java b/src/main/java/botcommons/config/Config.java index 384eeb9..ccdfda8 100644 --- a/src/main/java/botcommons/config/Config.java +++ b/src/main/java/botcommons/config/Config.java @@ -14,9 +14,10 @@ public class Config extends HashMap { private static Config instance; /** - * Creates a config file with the given values and bot name - * @param oldValues The values to put in the config - * @return The {@link Config} object + * Creates a new Config instance from the provided old values and bot name. + * @param oldValues A map containing old values to be used in the configuration. This can include key-value pairs that will be added to the new config. + * @param botName The name of the bot for which the configuration is being created. This will be used to set the "bot-name" key in the configuration. + * @return Returns a new instance of the Config class, which is a HashMap containing the provided old values and additional default values. */ public static Config makeConfig(Map oldValues, String botName) { HashMap values = new HashMap<>(); @@ -50,6 +51,12 @@ public static Config makeConfig(Map oldValues, String botName) { return instance; } + /** + * Creates a new Config instance from a JSON file. + * @param file The path to the JSON file from which to load the configuration. This file should contain a valid JSON representation of a Config object. + * @return Returns a Config object that has been populated with the data from the specified JSON file. This object will be an instance of the Config class, which extends HashMap. + * @throws IOException If an I/O error occurs while reading the file. This can happen if the file does not exist, is not accessible, or if there are issues with reading the file's contents. + */ public static Config makeConfig(String file) throws IOException { Config config = new GsonBuilder().setPrettyPrinting().create() .fromJson(Files.readString(new File(file).toPath()), Config.class); @@ -86,6 +93,10 @@ public Object get(Object key) { return get(key.toString(), String.class); } + /** + * Writes the current configuration to a JSON file. + * @throws IOException If an I/O error occurs while writing to the file. This can happen if the file is not writable, the directory does not exist, or there are permission issues. + */ public void write() throws IOException { Files.writeString(Path.of(Config.instance.get("bot-name") + "-assets", "config.json"), toString()); } diff --git a/src/main/java/botcommons/config/ConfigManager.java b/src/main/java/botcommons/config/ConfigManager.java index cd05ade..1247de9 100644 --- a/src/main/java/botcommons/config/ConfigManager.java +++ b/src/main/java/botcommons/config/ConfigManager.java @@ -34,26 +34,54 @@ public ConfigManager(String botName, JDA jda) { instance = this; } + /** + * Initializes the ConfigManager with a default configuration. + * @param config The default configuration to be used if no specific server configuration is found. + */ public void setDefault(Config config) { configs.put("default", config); } + /** + * Sets the config for a server, and writes it to a file. + * @param serverId The ID of the server for which the configuration is being set. This will be used to create a unique file for the server. + * @param config The configuration object to be set for the server. This object will be serialized to JSON and saved to a file named after the server ID. + */ public void setConfig(String serverId, Config config) { configs.put(serverId, config); writeConfigToFile(serverId, config); } + /** + * Sets a specific value in the config for the server + * @param serverId The ID of the server for which the value is being set. This will determine which server's configuration will be updated. + * @param key The key under which the value will be stored in the configuration. This key will be used to retrieve the value later. + * @param value The value to be set in the configuration for the specified key. This can be any object, and it will be serialized to JSON when saved to the configuration file. + */ public void setValue(String serverId, String key, Object value) { Config config = configs.getOrDefault(serverId, configs.get("default")); config.put(key, value); writeConfigToFile(serverId, config); } + /** + * Retrieves a value from the configuration for a specific server. + * @param serverId The ID of the server for which the value is being retrieved. This will determine which server's configuration will be accessed. + * @param key The key for the value you want to retrieve from the configuration. This key should match the one used when setting the value. + * @param type The class type of the value you want to retrieve. This is used to deserialize the JSON value back into the appropriate Java type. For example, if you expect an Integer, you would pass Integer.class. + * @return The value associated with the specified key in the configuration for the given server. The return type will depend on the type parameter provided. If the key does not exist, it will return null. + * @param The type of the value to be returned. This is a generic type parameter that allows the method to return the value in the desired type. For example, if you expect an Integer, you would specify Integer.class when calling this method. + */ public T getValue(String serverId, String key, Class type) { Config config = configs.getOrDefault(serverId, configs.get("default")); return config.get(key, type); } + /** + * Write the configuration to a file for a specific server. + * @param serverId The ID of the server for which the configuration is being written. This will be used to create a unique filename for the configuration file. + * @param config The configuration object to be written to the file. This object will be serialized to JSON and saved to a file named after the server ID. + */ private void writeConfigToFile(String serverId, Config config) { final int MAX_RETRIES = 3; int attempts = 0; @@ -108,6 +136,8 @@ public void onGuildReady(@NotNull GuildReadyEvent event) { } else { configs.put(serverId, configs.get("default")); } + + writeConfigToFile(serverId, configs.get(serverId)); } } diff --git a/src/main/java/botcommons/menu/MenuManager.java b/src/main/java/botcommons/menu/MenuManager.java index b93bf45..ce7c5fb 100644 --- a/src/main/java/botcommons/menu/MenuManager.java +++ b/src/main/java/botcommons/menu/MenuManager.java @@ -22,12 +22,20 @@ public class MenuManager extends ListenerAdapter { public static MenuManager instance; + /** + * Creates a new instance of {@link MenuManager}. This will register the instance with JDA and add itself as an event listener. + * @param jda The JDA instance to register with. This allows the manager to listen for button interactions. + */ public MenuManager(JDA jda) { instance = this; this.jda = jda; jda.addEventListener(this); } + /** + * Registers one or more menus to the menu registry. This allows the manager to handle button interactions for these menus. + * @param menus The menus to register. Each menu should be annotated with {@link botcommons.menu.Menu} to provide an ID. + */ public static void registerMenu(IMenu... menus) { for (IMenu menu : menus) { String menuId = menu.getClass().getAnnotation(Menu.class).id(); @@ -36,6 +44,11 @@ public static void registerMenu(IMenu... menus) { } } + /** + * Registers a menu with a specific ID. This allows for dynamic registration of menus that may not be known at compile time. + * @param id The ID to register the menu with. This should be a unique identifier for the menu. + * @param menu The menu instance to register. This should implement the {@link IMenu} interface and provide the necessary functionality for handling button interactions. + */ public static void registerMenuWithId(String id, IMenu menu) { if (id == null || id.isEmpty()) { throw new IllegalArgumentException("Menu ID cannot be null or empty"); @@ -46,6 +59,11 @@ public static void registerMenuWithId(String id, IMenu menu) { menuRegistry.put(id, menu); } + /** + * Sends a menu to a specific text channel. This method builds the menu's embed and sends it as a message to the specified channel. + * @param menuId The ID of the menu to send + * @param channelId The channel to send it in + */ public static void sendMenu(String menuId, String channelId) { IMenu menu = menuRegistry.get(menuId); @@ -62,6 +80,11 @@ public static void sendMenu(String menuId, String channelId) { .queue(message -> menu.setMessageId(message.getId())); } + /** + * Sends a menu to a user's private channel. This method builds the menu's embed and sends it as a private message to the specified user. + * @param menuId The ID of the menu to send. This should correspond to a registered menu in the {@link MenuManager#menuRegistry}. + * @param userId The ID of the user to send the menu to. This should be a valid Discord user ID. The method will attempt to retrieve the user and send the private message. + */ public static void sendMenuPrivate(String menuId, String userId) { IMenu menu = menuRegistry.get(menuId); @@ -100,6 +123,12 @@ public static void replyMenu(String menuId, Message message, Object... args) { menu.setMessageId(message1.getId())); } + /** + * Replies to a message with a menu. This method builds the menu's embed and sends it as a reply to the specified interaction hook. + * @param menuId The ID of the menu to send. This should correspond to a registered menu in the {@link MenuManager#menuRegistry}. + * @param hook The interaction hook to reply to. This is typically obtained from a slash command interaction or button interaction. It allows you to send a message in response to the interaction. + * @param args The arguments to pass into constructing the menu instance. This allows for dynamic creation of menus based on runtime parameters. The arguments should match the constructor of the menu class if it has one. + */ public static void replyMenu(String menuId, InteractionHook hook, Object... args) { IMenu menu = getMenu(menuId, args); diff --git a/src/main/java/botcommons/menu/types/PageMenu.java b/src/main/java/botcommons/menu/types/PageMenu.java index bb11888..ff82fc0 100644 --- a/src/main/java/botcommons/menu/types/PageMenu.java +++ b/src/main/java/botcommons/menu/types/PageMenu.java @@ -61,7 +61,8 @@ public MessageEmbed build() { public Button[] getButtons() { return new Button[]{ Button.of(ButtonStyle.PRIMARY, "left", Emoji.fromUnicode("⬅️")), - Button.of(ButtonStyle.SECONDARY, "right", Emoji.fromUnicode("➡️")) + Button.of(ButtonStyle.SECONDARY, "right", Emoji.fromUnicode("➡️")), + Button.of(ButtonStyle.DANGER, "end", Emoji.fromUnicode("❌")) }; } } diff --git a/src/main/java/botcommons/utilities/JsonUtils.java b/src/main/java/botcommons/utilities/JsonUtils.java index 1858651..5c33286 100644 --- a/src/main/java/botcommons/utilities/JsonUtils.java +++ b/src/main/java/botcommons/utilities/JsonUtils.java @@ -16,7 +16,13 @@ import java.nio.file.Path; import java.util.HashMap; +/** + * Utility class for handling JSON serialization and deserialization using Gson. + */ public class JsonUtils { + /** + * The Gson instance used for JSON serialization and deserialization. + */ private static final Gson GSON = new GsonBuilder() .setExclusionStrategies(new ExclusionStrategy() { @Override @@ -35,6 +41,13 @@ public boolean shouldSkipClass(Class clazz) { .create(); + /** + * Create a cache file from a HashMap if it does not already exist. + * @param map The HashMap to serialize into a JSON file. This map will be converted to JSON and saved to a file. + * @param name The name of the cache file to be created. This name will be used to create the file path where the JSON data will be stored. The file will be saved in the "cache" directory with a ".json" extension. + * @param The key type + * @param The value type + */ public static void createCache(HashMap map, String name) { File file = StringUtilities.getAssetPath(Path.of("cache/",name+".json")).toFile(); if (file.exists()) return; @@ -46,6 +59,11 @@ public static void createCache(HashMap map, String name) { } } + /** + * Updates the cache file with the contents of the provided HashMap. If the cache file does not exist, it will be created. + * @param map The HashMap to serialize into a JSON file. This map will be merged with existing data in the cache file if it exists. + * @param name The name of the cache file to be updated. This name will be used to create the file path where the JSON data will be stored. The file will be saved in the "cache" directory with a ".json" extension. + */ public static void updateCache(HashMap map, String name) { Logger logger = LoggerFactory.getLogger(JsonUtils.class); File file = StringUtilities.getAssetPath(Path.of("cache/", name + ".json")).toFile(); diff --git a/src/main/java/botcommons/utilities/StringUtilities.java b/src/main/java/botcommons/utilities/StringUtilities.java index 27c3642..145fb96 100644 --- a/src/main/java/botcommons/utilities/StringUtilities.java +++ b/src/main/java/botcommons/utilities/StringUtilities.java @@ -15,12 +15,27 @@ public class StringUtilities { public static String botName = "default"; static Logger logger = LoggerFactory.getLogger(StringUtilities.class); + /** + * This method converts a HashMap with generic keys and values to a HashMap with String keys and String values by serializing each key and value to JSON format. + * @param map The input HashMap with generic keys and values. This map can contain any type of objects as keys and values. + * @return A new {@link HashMap} where each key and value from the input map has been serialized to a JSON string. This allows for easy storage or transmission of the map's contents in a standardized format. + */ public static HashMap stringifyMap(HashMap map) { HashMap stringMap = new HashMap<>(); Gson gson = new GsonBuilder().setPrettyPrinting().create(); map.forEach((key, value) -> stringMap.put(gson.toJson(key), gson.toJson(value))); return stringMap; } + + /** + * This method parses a HashMap of String keys and String values back into a Map with specified key and value types using Gson for deserialization. + * @param strings The input HashMap with String keys and String values. This map is expected to contain JSON strings that represent the keys and values of the desired output map. + * @param keyClass The class type of the keys in the output map. This parameter specifies the type to which the keys in the input map should be deserialized. For example, if you want the keys to be integers, you would pass Integer.class. + * @param valueClass The class type of the values in the output map. This parameter specifies the type to which the values in the input map should be deserialized. For example, if you want the values to be strings, you would pass String.class or if you want them to be integers, you would pass Integer.class. + * @return A new {@link Map} where each key and value from the input HashMap has been deserialized into the specified types. The keys and values in the output map will be of the types specified by the keyClass and valueClass parameters, respectively. This allows for type-safe access to the elements in the resulting map. + * @param The type of the keys in the output map. This is a generic type parameter that allows the method to return a map with keys of the specified type. For example, if you want the keys to be integers, you would specify Integer.class when calling this method. + * @param The type of the values in the output map. This is a generic type parameter that allows the method to return a map with values of the specified type. For example, if you want the values to be strings, you would specify String.class when calling this method, or if you want them to be integers, you would specify Integer.class. + */ @SuppressWarnings("unused") public static Map parseMap(HashMap strings, Class keyClass, Class valueClass) { HashMap map = new HashMap<>(); @@ -29,6 +44,11 @@ public static Map parseMap(HashMap strings, Class