Skip to content

Commands

Ryan edited this page Nov 7, 2024 · 13 revisions

This guide will show you how to create commands.

Table of Contents

Creation

Creating a command

In this example we're going to create a simple ping command.

To create a command, you should first create a class extending BaseCommand.
Next, annotate the class with CommandBuilder and set its properties (like the command's name and description).

// PingCommand.java
@CommandBuilder(
        name = "ping",
        description = "Pong!"
)
public class PingCommand extends BaseCommand {

}

Now, we should implement a command execution interface:

Command execution interfaces

  • MessageCommand - if this command is a message command
  • SlashCommand - if this command is a slash command
  • CombinedCommand - if this command is both a message and slash command. Instead of implementing this, you could also implement both of the other interfaces.

In this example we will create a combined command.

// PingCommand.java
@CommandBuilder(
        name = "ping",
        description = "Pong!"
)
public class PingCommand extends BaseCommand implements CombinedCommand {

}

Next, implement the abstract methods:

// PingCommand.java
@CommandBuilder(
    name = "ping",
    description = "Pong!",
    category = "awesome" // We will register this category in the next step. This field is case-insensitive.
)
public class PingCommand extends BaseCommand implements CombinedCommand {
    @Override
    public ArgumentSet getArguments() {
        return null; // this command will have no arguments, so we will return null
    }

    @Override
    public void execute(CommandEvent event) throws CommandException {
        // code to run when the command is executed
        event.reply("Pong!");
    }
}

Congratulations! You've created your first command.

However, we're not done yet: we still have to register our command.
We can let Colossus automatically register our commands by using the scanPackage method. This will scan all classes in the specified package for commands and register them. Let's take a look at an example below. We will create a category for our command as well.

// Main.java
Colossus bot = new ColossusBuilder()
    .registerCategories(
        new Category("Awesome", "All awesome commands.", "🌟")
    )
    .scanPackage("dev.ryanland.firstbot")
    .build();
bot.initialize(); // initialize the bot

Creating a subcommand

First, create a subcommand holder class. This will act as a command containing its subcommands.

Context commands cannot have subcommands.

  1. Make a subclass of BaseCommand
  2. Annotate the class with CommandBuilder and set its properties
  3. Implement the SubCommandHolder interface
  4. Implement the abstract methods
@CommandBuilder(
    name = "member",
    description = "Perform an action to a member"
)
public class MemberCommand extends BaseCommand implements SubCommandHolder {
    @Override
    public ArgumentSet getArguments() {
        return null; // getArguments will have no effect on SubCommandHolders, so just return null
    }
    
    @Override
    public List<SubCommand> registerSubCommands() {
        // register subcommands here
        // ...
    }
    
    // SubCommandHolders do not have an execution method
}

Next, create a subcommand.
5. Make a new subclass of BaseCommand
6. Annotate the class with CommandBuilder and set its properties, name being just the subcommand name
7. Implement the SubCommand interface
8. Implement a command execution interface
9. Implement the abstract methods

@CommandBuilder(
    name = "kick", // subcommand name; command would be "/member kick"
    description = "Kicks a member"
)
public class MemberKickCommand extends BaseCommand implements SubCommand, CombinedCommand {
    @Override
    public ArgumentSet getArguments() {
        return new ArgumentSet().addArguments(
            new MemberArgument()
                .name("member")
                .description("The member to kick")
        );
    }
    
    @Override
    public void execute(CommandEvent event) throws CommandException {
        // subcommand execution code
        ColossusMember member = event.getArgument("member");
        event.getGuild().kick(member).queue();
        event.reply("Member kicked!");
    }
}
  1. Register the subcommand in the subcommand holder
    @Override
    public List<SubCommand> registerSubCommands() {
        return List.of(new MemberKickCommand());
    }
  1. Finally, make sure your subcommand holder (not subcommands) is registered in your ColossusBuilder. This is the same as registering a regular command (so it may be automatic).

Creating a nested subcommand/subcommand group

In this example, we will make a nested subcommand called /member action kick, action being the subcommand group.
We will follow up on the tutorial above.

To create a nested subcommand holder:

  1. Make a subclass of BaseCommand
  2. Annotate the class with CommandBuilder and set its properties
  3. Implement the SubCommandHolder and SubCommand interface
  4. Implement the abstract methods
  5. Provide the nested subcommands
@CommandBuilder(
    name = "action",
    description = "Perform an action"
)
public class MemberActionCommand extends BaseCommand implements SubCommandHolder, SubCommand {
    @Override
    public ArgumentSet getArguments() {
        return null; // getArguments will have no effect on SubCommandHolders, so just return null
    }

    @Override
    public List<SubCommand> registerSubCommands() {
        // register nested subcommands
        return List.of(new MemberKickCommand());
    }
}
  1. Register the nested subcommand holder (not subcommands of this nested holder) in the top subcommand holder
// MemberCommand.java
    @Override
    public List<SubCommand> registerSubCommands() {
        return List.of(new MemberActionCommand());
    }

Creating a context command

In this example we will create a message context command, replying with the character length of the message.

  1. Make a subclass of ContextCommand<T>
    T should be either User or Message, depending on the type of context command you're looking to create
  2. Annotate the class with ContextCommandBuilder and set its properties
  3. Implement the run method
@ContextCommandBuilder(
    name = "Character length"
)
public class TestContextCommand extends ContextCommand<Message> {
    @Override
    public void run(ContextCommandEvent<Message> event) throws CommandException {
        Message message = event.getTarget(); // get the context target (User or Message)
        int length = message.getContentRaw().length(); // get the length of the Message
        event.reply("Length: " + length); // reply with the result
    }
}

Arguments

Using arguments

Let's create a ban command. For this command, we will need two arguments: the member to ban and the reason.

Context commands cannot have arguments.

Colossus provides an argument system. In this case, we will use the MemberArgument for the member and the EndlessStringArgument for the reason.
The endless string will be a regular string argument for slash commands, but for message commands, it will get everything following the argument.

All arguments must have a name and description. We are also going to make the reason argument have a maximum length of 500 characters, and the argument will be optional, returning "No reason provided" if the user didn't provide anything.

@CommandBuilder(
    name = "ban",
    description = "Ban someone."
)
public class BanCommand extends BaseCommand implements CombinedCommand {
    @Override
    public ArgumentSet getArguments() {
        return new ArgumentSet().addArguments(
            new MemberArgument()
                .name("member")
                .description("The member to ban"),
            new EndlessStringArgument()
                .setMaximum(500) // maximum reason length
                .name("reason")
                .description("The reason")
                .optional(event -> "No reason provided") // optional value
        );
    }

    @Override
    public void execute(CommandEvent event) throws CommandException {
        // ...
    }
}

The argument's values can be retrieved using the event.getArgument("name") method.
If the user provides no reason, this method will return "No reason provided" as we defined earlier.

    @Override
    public void execute(CommandEvent event) throws CommandException {
        ColossusMember member = event.getArgument("member"); // member argument
        String reason = event.getArgument("reason"); // reason argument

        event.getGuild().ban(member, 0, reason).queue(); // ban the member
        event.reply("Member banned!");
    }

Creating custom argument types

You can create your own argument types by extending the Argument<T> class, T being the argument return type.

For message commands, arguments are given a Deque<String> of the remaining parameters for the command (parameters split by " ").
For slash commands, arguments are given a Deque<OptionMapping> of the remaining parameters.

Arguments must return CompletableFuture<T> by default.

There are other subclasses to extend from instead to make life easier:

  • SingleArgument - argument only demanding one value from the argument queue in the parser, returning args.pop()
  • FutureSingleArgument - same as SingleArgument, except for the return type being a CompleteableFuture<T>
  • NumberArgument - extends SingleArgument - catches NumberFormatExceptions in resolvers, then sends an error
  • ArgumentStringResolver - extends SingleArgument - combines the message and slash command resolver for strings
  • FutureArgumentStringResolver - extends FutureSingleArgument - same as ArgumentStringResolver, except for the return type being a CompletableFuture<T>

Let's create an argument that returns the amount of +'s provided. For this we should extend ArgumentStringResolver<Integer>.

Throwing an ArgumentException in a resolver method will automatically make the bot send a detailed error message, along with the exception message.

public class PlusesArgument extends ArgumentStringResolver<Integer> {
    
    @Override
    public Integer resolve(String arg, CommandEvent event) throws ArgumentException {
        if (!arg.contains("+")) { // if the argument contains characters other than '+', error
            throw new MalformedArgumentException("Only expected pluses (+)");
        }
        if (arg.length() > 5) { // if the argument has more than 5 '+', error
            throw new MalformedArgumentException("You cannot specify more than 5 pluses");
        }
        return arg.length(); // return the amount of '+'
    }
}

Instead of this approach, we could also give the user some options, if they are using a slash command. Because we want different behaviour for slash and message commands, we're going to use a SingleArgument<Integer>.

You can add argument options by overriding the getArgumentOptionData() method. With this method, you can set various types of properties about a slash command option, such as its type, minimum value, etc.

Result:

public class PlusesArgument extends SingleArgument<Integer> {

    @Override
    public ArgumentOptionData getArgumentOptionData() {
        return (ArgumentOptionData) new ArgumentOptionData(OptionType.INTEGER)
            .addChoice("+", 1)
            .addChoice("++", 2)
            .addChoice("+++", 3)
            .addChoice("++++", 4)
            .addChoice("+++++", 5);
    }

    @Override
    public Integer resolveSlashCommandArgument(OptionMapping arg, SlashCommandEvent event) throws ArgumentException {
        return arg.getAsInt();
    }

    @Override
    public Integer resolveMessageCommandArgument(String arg, MessageCommandEvent event) throws ArgumentException {
        if (!arg.contains("+")) {
            throw new MalformedArgumentException("Only expected pluses (+)");
        }
        if (arg.length() > 5) {
            throw new MalformedArgumentException("You cannot specify more than 5 pluses");
        }
        return arg.length();
    }
}

One final, more complicated approach we could take for a message command, is presenting the user with a menu with buttons to pick an amount of pluses.

For this, we need to use futures. We will extend Argument<Integer>, and the slash command should keep the same behaviour.

public class PlusesArgument extends Argument<Integer> {

    @Override
    public ArgumentOptionData getArgumentOptionData() {
        return (ArgumentOptionData) new ArgumentOptionData(OptionType.INTEGER)
            .addChoice("+", 1)
            .addChoice("++", 2)
            .addChoice("+++", 3)
            .addChoice("++++", 4)
            .addChoice("+++++", 5);
    }

    @Override
    public CompletableFuture<Integer> resolveSlashCommandArgument(Deque<OptionMapping> args, SlashCommandEvent event) throws ArgumentException {
        CompletableFuture<Integer> future = new CompletableFuture<>();
        future.complete(args.pop().getAsInt());
        return future;
    }

    @Override
    public CompletableFuture<Integer> resolveMessageCommandArgument(Deque<String> args, MessageCommandEvent event) throws ArgumentException {
        // ...
    }
}

We don't need a command input from the user beforehand, so we will not touch the args queue.
Because there is no input, we should ignore an MissingArgumentException by overriding the ignoreMissingException() method.

We'll create a simple menu using PresetBuilder. In our button consumer, we'll complete the future and set the RepliableEvent for the command event to our button click event: so we can reply to our command correctly.

Result:

    @Override
    public boolean ignoreMissingException() {
        return true;
    }

    @Override
    public CompletableFuture<Integer> resolveMessageCommandArgument(Deque<String> args, MessageCommandEvent event) throws ArgumentException {
        CompletableFuture<Integer> future = new CompletableFuture<>();
        long userId = event.getUser().getIdLong();

        PresetBuilder message = new PresetBuilder("Pick an amount of pluses")
            .addButtons(
                // when a button is clicked, the future is completed
                BaseButton.user(userId, Button.secondary("1", "+"), clickEvent -> {
                    event.setRepliableEvent(clickEvent); // important to set the RepliableEvent before the future is completed
                    future.complete(1); // complete the future + execute the command with this value
                }),
                BaseButton.user(userId, Button.secondary("2", "++"), clickEvent -> {
                    event.setRepliableEvent(clickEvent);
                    future.complete(2);
                }),
                BaseButton.user(userId, Button.secondary("3", "+++"), clickEvent -> {
                    event.setRepliableEvent(clickEvent);
                    future.complete(3);
                }),
                BaseButton.user(userId, Button.secondary("4", "++++"), clickEvent -> {
                    event.setRepliableEvent(clickEvent);
                    future.complete(4);
                }),
                BaseButton.user(userId, Button.secondary("5", "+++++"), clickEvent -> {
                    event.setRepliableEvent(clickEvent);
                    future.complete(5);
                })
            );

        event.reply(message);
        return future;
    }

List of default argument types

Below is a list of default argument types provided by Colossus

Name Slash command type Slash command description Message command description Extra parameters Return type
Primitive arguments
BooleanArgument Boolean Returns true if 'true', otherwise returns false Boolean
IntegerArgument Integer Parses String as Integer min, max Integer
DoubleArgument Number Parses String as Double min, max Double
FloatArgument Number Casts Double to Float Parses String as Float min, max Float
LongArgument Number Parses String as Long min, max Long
String arguments
StringArgument String min, max String
QuoteStringArgument String Normal string Returns all text until a closing " min, max String
EndlessStringArgument String Returns all text in the following arguments min, max String
Snowflake arguments
UserArgument User Gets the user using a mention or ID ColossusUser
MemberArgument User Throws an exception if the provided user is not a member of this server Gets the member using a mention or ID ColossusMember
RoleArgument Role Gets the role using a mention or ID Role
GuildChannelArgument Channel Throws an exception if the provided channel is not permitted permittedChannelTypes GuildChannel
GuildArgument String Gets the guild using an ID Gets the guild using an ID ColossusGuild
AttachmentArgument Attachment Gets the message attachment, multiple are supported Attachment
Command arguments
BasicCommandArgument String Same as message command, except has all command names as autocompleteable options Gets a BasicCommand (regular or context) using its name, and if multiple results are found, show a menu with choices BasicCommand
CommandArgument String Same as message command, except has all regular command names as autocompleteable options Gets a regular Command using its name Command
ContextCommandArgument String Same as message command, except has all context command names as autocompleteable options Gets a ContextCommand (user or message) using its name, and if multiple results are found, show a menu with choices ContextCommand<?>
UserContextCommandArgument String Same as message command, except has all user context command names as autocompleteable options Gets a user ContextCommand using its name ContextCommand<User>
MessageContextCommandArgument String Same as message command, except has all message context command names as autocompleteable options Gets a message ContextCommand using its name ContextCommand<Message>

Permissions

Slash, message and context commands can have permissions.

Adding permissions to a command

  1. Override the getPermission() method in your command
  2. Create a PermissionBuilder
    @Override
    public PermissionHolder getPermission() {
        return new PermissionBuilder()
            .addRequirements(
                // ...
            )
            .build();
    }

Using the permission builder, we can add requirements to running the command.

Default permission requirement types

  • BotOwnerRequirement - needs to be bot owner
  • JDAPermissionRequirement - needs to have permission
  • PermissionHolderRequirement - needs to meet all permissions in the (nested) holder
  1. Add one or more permission requirements
    @Override
    public PermissionHolder getPermission() {
        return new PermissionBuilder()
            .addRequirements(
                new JDAPermissionRequirement(Permission.BAN_MEMBERS), // the member executing the command must have the "Ban Members" permission
                new BotOwnerRequirement() // the user executing the command must also be the bot owner
            )
            .add(Permission.KICK_MEMBERS) // shortcut to JDAPermissionRequirement
            .build(); // build into a PermissionHolder
        }

Now, the command will not execute and send an error instead if the user does not meet all of the permission requirements.

Instead of using build(), we could also use buildOptional(): if this method is used, the command will execute if at least one of the requirements is met.

Setting default Discord member permissions

Only slash and context commands can have default Discord member permissions.

We can specify a set of permissions that sets who can use a command by default, Discord will disable the slash command for members who do not have permission. Admins of guilds can change these permissions and restrict commands to specific members/roles. Members with Permission.ADMINISTRATOR can bypass any permissions set.

The permission requirement system completely stands alone from this: the requirements will still be checked, regardless of what is set here.

To set the default permissions, first override the getDefaultPermissions() method in your command, and then create a DefaultMemberPermissions.

DefaultMemberPermissions.DISABLED can be used to only allow administrators by default, and DefaultMemberPermissions.ENABLED can be used to allow all members by default. If no default permissions are set, Colossus will use DefaultMemberPermissions.DISABLED if the command is currently disabled and DefaultMemberPermissions.ENABLED if enabled.

In the example below, the slash command will be available only to members with the "Ban Members" permission by default.

    @Override
    public DefaultMemberPermissions getDefaultPermissions() {
        return DefaultMemberPermissions.enabledFor(Permission.BAN_MEMBERS);
    }

Creating custom permission requirement types

You can create your own permission requirement types by implementing the PermissionRequirement interface.

This interface contains 2 abstract methods:

  • boolean check(BasicCommandEvent event)
    This method decides whether this requirement should pass or not, given the event. Should return true if the requirement is met, false if not.
  • String getName()
    This method should return the name of this requirement. It will be used in e.g. the error message for if the requirement is not met.

Example requirement:

public class MemberAgeRequirement implements PermissionRequirement {
    @Override
    public boolean check(BasicCommandEvent event) {
        // true if the member join date is before the current time minus 24 hours, false otherwise
        return event.getMember().getTimeJoined().isBefore(OffsetDateTime.now().minusDays(1));
    }

    @Override
    public String getName() {
        return "24 Hour Member Age";
    }
}

Cooldowns

Slash, message and context commands can have cooldowns.

Adding a cooldown to a command

You can set a cooldown of a command by setting it in its Builder (ContextCommandBuilder or CommandBuilder). This value is in seconds, and it is 2 by default.

@CommandBuilder(
    name = "ban", 
    description = "Ban someone",
    cooldown = 7.5F // cooldown of 7.5 seconds
)

Cooldowns are managed using a CooldownManager. By default, the MemoryCooldownManager is used: this manager will store cooldowns in memory. This also means that cooldowns will be reset on restart!

There is one other built-in cooldown manager: DatabaseCooldownManager. This manager will use the configured database system. Before using this manager, database cooldowns should be enabled using ColossusBuilder#enableDatabaseCooldowns. This method will create a cooldowns table inside of your database.

Cooldowns stored in a database are useful for things like daily rewards - commands with large cooldowns.

To use another cooldown manager, override the getCooldownManager() method in your command. Example:

    @Override
    public CooldownManager getCooldownManager() {
        return DatabaseCooldownManager.getInstance();
    }

Creating a custom cooldown manager

Create a custom cooldown manager by implementing the CooldownManager interface.

Refer to the JavaDoc for more details about the methods.

Inhibitors

Slash, message and context commands support inhibitors.

Inhibitors are conditions that are checked before a command is run, and an error will be sent if a condition is not met.
Colossus uses inhibitors internally to check things as well, e.g. for checking if a cooldown is currently active.

Creating an inhibitor

You can create an inhibitor by implementing the Inhibitor interface.

This interface contains 2 abstract methods:

  • boolean check(BasicCommandEvent event) This method decides whether this inhibitor is accepted or not, given the event. Should return true if the inhibitor is not accepted (thus resulting in an error and the command not being executed), false otherwise.
  • PresetBuilder buildMessage(BasicCommandEvent event) This method should return the message sent when the inhibitor is not accepted

Register your inhibitor using the ColossusBuilder#registerInhibitors method.

Example inhibitor:

public class UsernameInhibitor implements Inhibitor {
    @Override
    public boolean check(BasicCommandEvent event) {
        return event.getUser().getName().equals("Ryan"); // inhibitor is not accepted if username equals 'Ryan'
    }

    @Override
    public PresetBuilder buildMessage(BasicCommandEvent event) {
        return new PresetBuilder("People named 'Ryan' may not use commands");
    }
}

Finalizers

Slash, message and context commands support finalizers.

Finalizers are executed when a command has finished running without errors.
Colossus uses finalizers internally to finalize commands as well, e.g. setting the cooldown.

Creating a finalizer

You can create a finalizer by implementing the Finalizer interface.

Register your finalizer using the ColossusBuilder#registerFinalizers method.

Example finalizer:

public class CoinFinalizer implements Finalizer {
    @Override
    public void finalize(BasicCommandEvent event) {
        event.getUser().modifyValue("coins", coins -> (int) coins + 1); // give the user a coin
    }
}

Localization

Slash and context commands support command localization.

Command names, descriptions, arguments, etc. can have localizations depending on the user's Discord client's language.
The fallback language is US English.

Setting a localization function

You can set the localization function used by every command by default in Colossus using the ColossusBuilder#setLocalizationFunction method.

The localization key is composed of the command/option/choice tree being walked, where each command/option/choice's name is separated by a dot.
The key is also in lowercase and spaces are replaced by underscores.

A few examples of localization keys:

  • The name of a command named "ban": ban.name
  • The name of a message context named "Get content raw": get_content_raw.name
  • The description of a command named "ban": ban.description
  • The name of a subcommand "perm" in a command named "ban": ban.perm.name
  • The description of an option "duration" in a subcommand "perm" in a command named "ban": ban.perm.duration.description
  • The name of a choice in an option "duration" in a subcommand "perm" in a command named "ban": ban.perm.duration.choice.name

Using a resource bundle localization function

A resource bundle localization function is a localization function which will retrieve localization strings from specified bundles.
Translation files would be placed in the resources directory. These files contain key-value pairs representing the translation keys and their corresponding translations.

/*
 * This function will use the localizations set in the files "Translations_nl.properties" and "Translations_de.properties"
 * under the resources directory
 */
ResourceBundleLocalizationFunction
    .fromBundles("Translations", DiscordLocale.DUTCH, DiscordLocale.GERMAN)
    .build()

Setting a unique localization function for a command

By default, commands will use the localization function defined in your ColossusBuilder.
However, you can make an exception for this in a command by overriding the getLocalizationFunction() method.

Default commands

Colossus provides some commands (slash + message) by default.

Help command

Colossus provides you with a help command, this command will show you information about the specified command, supporting subcommands with pages, or if no command is specified, sends a list of commands divided into their respective (sub)categories using pages.

Disabling

The default help command can be disabled using ColossusBuilder#disableHelpCommand.