Commands (1.1.0+)

Most plugins will rely on clients sending commands to them for at least some functions such as re-loading configuration, registration of users/channels/clients of some sort or for informational purposes.

Typed commands

The framework allows plugins to descriptively register commands using a builder pattern. Plugins define the command, its aliases, a permission that may be required and whether or not their command uses arguments or parameters. Lastly, plugins define what executor will consume the command context in order to perform the requested action.

As the name suggests, typed commands allow typed access to a commands parameters or arguments. The users input for a parameter or argument will be matched to an instance of the desired class before the command executor is invoked and will be available during execution.

Parameters vs. Arguments

Commands can either have parameters or arguments. Parameterized commands are very similar to the traditional commands (!command <param1> <param2> "<quoted param3>") where the order of the parameters decides what string should be resolved to what type.
Commands that use arguments on the other hand are more like command line commands where argument names have to be specified. (!arg-command --param1=<value> --param3="<quoted value>" --param2=<value>)
When arguments are used, the order does no longer matter.

Registering commands as a plugin

Let's look at an example. The following command specification describes a command "!mycommand" of a plugin called "myplugin" which will take a teamspeak channel as a parameter.
As can be seen, the command is registered with an alias that contains the plugin ID. This is considered good practice as it allows administrators and users to access this specific command even when another plugin overwrites the command.

import static de.fearnixx.jeak.service.command.spec.Commands.*;

public class MyCommand {
  public static ICommandSpec getCommandSpec() {
    return commandSpec("mycommand", "myplugin:mycommand")
      .parameters(
        paramSpec("channel", IChannel.class)
      )
      .executor(new MyCommand()::execute)
      .build();
  }
    // ...
}

When a user now invokes the command !mycommand myChannel, an internal matcher will try to resolve "myChannel" into an object of IChannel by searching for it in the channel cache. If such a channel is not found - or multiple channels match the given name, an error will be shown to the user providing them with information about why the command could not be executed.
If one channel was found, the matcher will store the reference in the command execution context for the executor to access it.
The following shows the executor of "mycommand" and how it is able to retrieve the channel reference.

public class MyCommand {
    // ...
  private void execute(ICommandExecutionContext ctx) {
    Optional<IChannel> optChannel = ctx.getOne("channel", IChannel.class);
    
    if (optChannel.isPresent()) {
        // do stuff with that channel
    }
  }
  // ...
}

When an executor is certain that a parameter will always be available, it is allowed to use the #getRequiredOne(String, Class<T>)-getter instead. This allows developers to skip the unnecessary optional check:

public class MyCommand {
    // ...
  private void execute(ICommandExecutionContext ctx) {
    IChannel channel = ctx.getRequiredOne("channel", IChannel.class);
    // do stuff with that channel
  }
  // ...
}

Meta-Parameters

In addition to typed parameters/arguments, there are some additional typed to allow for a more flexible way of parameter combinations.

Optional

Optional arguments or parameters are not required for a command to be executed. Thus, optional parameters must be checked for presence by the developer. This also means that any parameter (or argument) not explicitly made optional, is a required one.

First-Of

Tell the command service that of all contained parameters (or arguments) one must be present. The first one to be successfully resolved will be used.

Supported types

At the moment, the following types can be used for arguments or parameters:

  • BigDecimal
  • BigInteger
  • Boolean
  • IChannel
  • IClient
  • IUser
  • Double
  • Integer
  • String
  • ISubject
    This list is extracted from here

Custom types

Plugins may allow custom types to be resolved as arguments or parameters. This step is performed by so-called type matchers and defined by the ICriterionMatcher interface. Most plugins will be able to extend AbstractTypeMatcher which will handle the string extraction for them.
Matchers return a result that tells the command implementation whether or not the given string/parameter could be successfully resolved.
Important note: Matchers keep track of the currently parsed index in parameterized commands. When they have been successfully applied, the index has to be incremented for the next parameter to be resolved. ICommandExecutionContext#getParameterIndex().incrementAndGet()

Storing results

Matchers set their result using #putOrReplaceOne on the execution context. The name of the key should always be the full parameter or argument name retrieved via IMatchingContext#getArgumentOrParamName.

Registering

Matchers are registered with the IMatcherRegistryService. Overwriting matchers is possible but limited to all commands registered after the overwrite was performed.

❗️

On state in type matchers.

Type matchers must be stateless in nature as they will be invoked asynchronously for every command execution where they are required, possibly simultaneously.
In general, matcher classes should only have fields that contain business services required for lookup by the resolve process. Storing information in fields of type matchers is bad practice and will almost certainly lead to commands breaking after being invoked.