-
Notifications
You must be signed in to change notification settings - Fork 3
Commands
This guide will show you how to create commands.
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:
MessageCommand- if this command is a message commandSlashCommand- if this command is a slash commandCombinedCommand- 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 botFirst, create a subcommand holder class. This will act as a command containing its subcommands.
Context commands cannot have subcommands.
- Make a subclass of
BaseCommand - Annotate the class with
CommandBuilderand set its properties - Implement the
SubCommandHolderinterface - 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!");
}
}- Register the subcommand in the subcommand holder
@Override
public List<SubCommand> registerSubCommands() {
return List.of(new MemberKickCommand());
}- 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).
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:
- Make a subclass of
BaseCommand - Annotate the class with
CommandBuilderand set its properties - Implement the
SubCommandHolderandSubCommandinterface - Implement the abstract methods
- 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());
}
}- 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());
}In this example we will create a message context command, replying with the character length of the message.
- Make a subclass of
ContextCommand<T>
Tshould be eitherUserorMessage, depending on the type of context command you're looking to create - Annotate the class with
ContextCommandBuilderand set its properties - 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
}
}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!");
}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, returningargs.pop()FutureSingleArgument- same asSingleArgument, except for the return type being aCompleteableFuture<T>NumberArgument- extendsSingleArgument- catchesNumberFormatExceptions in resolvers, then sends an errorArgumentStringResolver- extendsSingleArgument- combines the message and slash command resolver for stringsFutureArgumentStringResolver- extendsFutureSingleArgument- same asArgumentStringResolver, except for the return type being aCompletableFuture<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;
}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> |
Slash, message and context commands can have permissions.
- Override the
getPermission()method in your command - Create a
PermissionBuilder
@Override
public PermissionHolder getPermission() {
return new PermissionBuilder()
.addRequirements(
// ...
)
.build();
}Using the permission builder, we can add requirements to running the command.
BotOwnerRequirement- needs to be bot ownerJDAPermissionRequirement- needs to have permissionPermissionHolderRequirement- needs to meet all permissions in the (nested) holder
- 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.
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);
}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 returntrueif the requirement is met,falseif 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";
}
}Slash, message and context commands can have cooldowns.
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();
}Create a custom cooldown manager by implementing the CooldownManager interface.
Refer to the JavaDoc for more details about the methods.
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.
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 returntrueif the inhibitor is not accepted (thus resulting in an error and the command not being executed),falseotherwise. -
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");
}
}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.
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
}
}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.
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
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()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.
Colossus provides some commands (slash + message) by default.
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.
The default help command can be disabled using ColossusBuilder#disableHelpCommand.