Awesome
Command Framework
A generic CDI-based command framework. This library requires Java 8 or newer but is fully Java 9+ compatible and can run
as a proper Java module on the module path. Any arbitrary underlying message framework like a Discord library, an IRC
library, or a Skype library can be used by providing an according CommandHandler
implementation. You are also welcome to contribute such implementations back to the main project for all users benefit.
Table of Contents
- Prerequisites
- Supported Message Frameworks
- Setup
- Usage
- Message Framework
- Creating Commands
- Command Aliases
- Asynchronous Command Execution
- Command Description
- Command Restrictions
- Command Usage
- Parsing Parameters
- Customizing Parameter Converters
- Customizing Command Prefix
- Customizing Alias Calculation
- Customizing the Command Recognition and Resolution Process
- Storing Additional Data in the Command Context
- CDI Events
- Getting the Library Version Programmatically
- Supporting other Message Frameworks
- Version Numbers
- License
Prerequisites
- Java 8+
- At least one of the supported message frameworks unless a custom
CommandHandler
is used; without one there will be no error, but this framework will simply have nothing to do - An implementation of CDI that implements CDI 3.0.0 like Weld SE
- [Optional] ANTLR runtime 4.7.2 if the
ParameterParser
is used
Supported Message Frameworks
The following message frameworks are currently supported out of the box:
If you want to have support for an additional framework, do not hesitate to open a pull request or feature request issue.
Setup
Gradle
repositories { mavenCentral() }
dependencies { implementation 'net.kautler:command-framework:0.5.0' }
Maven
<dependency>
<groupId>net.kautler</groupId>
<artifactId>command-framework</artifactId>
<version>0.5.0</version>
</dependency>
Manually
Download the JAR for the latest release from the Latest Release Page and include it in your project.
Usage
Message Framework
Javacord
Text Commands
For the Javacord support, include Javacord as implementation dependency and create a CDI producer
that produces either one DiscordApi
, or if you use sharding a Collection<DiscordApi>
with all shards where you want
commands to be handled. You should also have a disposer method that properly disconnects the produced DiscordApi
instances.
Example:
@ApplicationScoped
class JavacordProducer {
@Inject
Logger logger;
@Inject
@Named
String discordToken;
@Produces
@ApplicationScoped
DiscordApi produceDiscordApi() {
return new DiscordApiBuilder()
.setToken(discordToken)
.login()
.exceptionally(ExceptionLogger.get())
.join();
}
private void disposeDiscordApi(@Disposes DiscordApi discordApi) {
discordApi.disconnect();
}
}
Tested versions:
3.4.0
Slash Commands
For slash commands you need to do the same as for Text Commands.
Additionally, the commands have to implement
SlashCommandJavacord
instead of Command
. Furthermore, they must have a
description defined, and all aliases have to consist of one to three
slash separated parts, so either command
, command/subcommand
, or command/subcommand-group/subcommand
.
If your command needs parameters, you overwrite the SlashCommandJavacord#getOptions
method, which just returns
an empty list in its default implementation. In the implementation of the method you can then use the full API
for Javacord slash command options.
When using command context transformers with slash
commands, all phases before BEFORE_COMMAND_COMPUTATION
are skipped by the command handler already.
Finally, this framework does not register the slash commands with Discord for you automatically, because you
might want to register them globally, or per server, or you might want to add further commands not managed by
this framework, and so on. Instead, you can register the commands yourself by injecting the provided
List<SlashCommandBuilder>
. This injected list can directly be used as argument for methods like
DiscordApi#bulkOverwriteGlobalApplicationCommands
or DiscordApi#bulkOverwriteServerApplicationCommands
.
The requirements described above regarding description and aliases are only enforced if you inject it somewhere.
A fully self-contained example containing a text, a slash, and a combined command can be found at
examples/simplePingBotJavacord
.
Example:
@ApplicationScoped
public class SlashCommandRegisterer {
@Inject
DiscordApi discordApi;
@Inject
List<SlashCommandBuilder> slashCommandBuilders;
void registerSlashCommands(@Observes @Initialized(ApplicationScoped.class) Object __) {
discordApi
.bulkOverwriteGlobalApplicationCommands(slashCommandBuilders)
.exceptionally(ExceptionLogger.get());
}
}
Tested versions:
3.4.0
JDA
For the JDA support, include JDA as implementation dependency and create a CDI producer that produces
either one JDA
, of if you use sharding a Collection<JDA>
, a ShardManager
or a Collection<ShardManager>
with all
shards where you want commands to be handled. You should also have a disposer method that properly shuts down the
produced JDA
and / or ShardManager
instances.
Example:
@ApplicationScoped
class JdaProducer {
@Inject
Logger logger;
@Inject
@Named
String discordToken;
@Produces
@ApplicationScoped
JDA produceJda() {
try {
return JDABuilder
.createLight(discordToken)
.build()
.awaitReady();
} catch (InterruptedException | LoginException e) {
logger.error("Exception while logging in to Discord", e);
return null;
}
}
private void disposeJda(@Disposes JDA jda) {
jda.shutdown();
}
}
Tested versions:
4.4.0_352
Creating Commands
Create a CDI bean that implements the Command
interface.
Example:
@ApplicationScoped
class PingCommand implements Command<Message> {
@Override
public void execute(CommandContext<? extends Message> commandContext) {
commandContext
.getMessage()
.getChannel()
.sendMessage("pong: " + commandContext.getParameterString().orElse(""))
.exceptionally(ExceptionLogger.get());
}
}
With everything else using the default, this is already enough to have a working ping bot.
A fully self-contained example can be found at examples/simplePingBotJavacord
.
To further customize the behavior of a command, you can either annotate the command class or overwrite the corresponding methods. Annotations are ignored when the corresponding methods are overwritten, but they can still be separately evaluated or used as documentation. For all functionality this framework uses the command method implementations. The annotations are only read in the default implementations of those methods.
Command Aliases
By overwriting the Command#getAliases()
method or applying one or multiple @Alias
annotations, the
aliases to which the command reacts can be configured. If no aliases are configured, the class name, with the Command
or Cmd
suffix / prefix stripped and the first letter lowercased is used as a default. If at least one alias is
configured, only the explicitly configured ones are used.
Javacord slash command specific: When injecting a List<SlashCommandBuilder>
anywhere, all aliases of commands
implementing SlashCommandJavacord
have to follow a pre-defined format that is described
at slash commands.
Asynchronous Command Execution
By overwriting the Command#isAsynchronous()
method or applying the @Asynchronous
annotation, the command handler can be told to execute the command asynchronously.
How exactly this is implemented is up to the command handler that evaluates this command. Usually the command will be executed in some thread pool. But, it would also be valid for a command handler to execute each asynchronous command execution in a new thread, so using this can add significant overhead if overused. If a command is not doing long-running or blocking operations, it may be preferable to not execute the command asynchronously. Although, if long-running or blocking operations are done in the command code directly, it may be preferable to execute the command asynchronously, as (depending on the underlying message framework) message dispatching could be blocked, introducing serious lag to the command execution.
As the command executions are potentially done on different threads, special care must be taken, if the command holds state, to make sure this state is accessed in a thread-safe manner. This can of course also happen without the command being configured asynchronously if the underlying message framework dispatches message events on different threads.
Command Description
By overwriting the Command#getDescription()
method or applying the @Description
annotation,
the description of the command can be configured. This description can be used, for example, in a custom help command.
Javacord slash command specific: When injecting a List<SlashCommandBuilder>
anywhere, all commands implementing
SlashCommandJavacord
have to provide a description.
Command Restrictions
By overwriting the Command#getRestrictionChain()
method or applying one or multiple
@RestrictedTo
annotations, and optionally the
@RestrictionPolicy
annotation, the restriction rules for a command can be configured. If
multiple @RestrictedTo
annotations are present and the default implementation of the method is used, a
@RestrictionPolicy
annotation that defines how the single restrictions are to be combined is mandatory. With this
annotation the single restrictions can be combined using all-of, any-of, or none-of logic.
For more complex boolean logic either overwrite the getRestrictionChain
method or provide custom CDI beans that
implement the Restriction
interface and contain the intended logic. For the latter also helpers
like ChannelJavacord
, RoleJavacord
,
ServerJavacord
, UserJavacord
, AllOf
,
AnyOf
, or NoneOf
can be used as super classes.
Examples:
@ApplicationScoped
class Vampire extends UserJavacord {
public Vampire() {
super(341505207341023233L);
}
}
@ApplicationScoped
class MyFancyServer extends ServerJavacord {
// make bean proxyable according to CDI spec
public MyFancyServer() {
super(-1);
}
@Inject
MyFancyServer(@Named("myFancyServerId") long myFancyServerId) {
super(myFancyServerId);
}
}
Command Usage
By overwriting the Command#getUsage()
method or applying the @Usage
annotation, the usage of the
command can be configured. This usage can be used, for example, in a custom help command.
When using the ParameterParser
, the usage string has to follow a pre-defined format that
is described at Parsing Parameters.
Parsing Parameters
There are three helpers to split the parameterString
that is provided to Command#execute(...)
into multiple
parameters that can then be handled separately.
Simple Splitting of Parameters
The first is the method Command.getParameters(...)
which you give the parameter string and the maximum amount of
parameters to split into. The provided string will then be split at any arbitrary amount of consecutive whitespace
characters. The last element of the returned array will have all remaining text in the parameter string. If you expect
exactly three parameters without whitespaces, you should set the max parameters to four, so you can easily test the
length of the returned array to determine if too many parameters were given to the command.
Semantic Parsing and Validation
The second is the ParameterParser
that you can get injected into your command. For the
ParameterParser
to work, the usage of the command has to follow a defined syntax language. This
usage syntax is then parsed and the given parameter string analysed according to the defined syntax. If the given
parameter string does not adhere to the defined syntax, a ParameterParseException
is thrown that can be caught and
reported to the user giving wrong arguments. The exception message is suitable to be directly forwarded to users.
The usage string has to follow this pre-defined format:
- Placeholders for free text without whitespaces (in the value) look like
<my placeholder>
- One placeholder for free text with whitespaces (in the value) is allowed as effectively last parameter and looks like
<my placeholder...>
- Literal parameters look like
'literal'
- Optional parts are enclosed in square brackets like
[<optional placeholder>]
- Alternatives are enclosed in parentheses and are separated by pipe characters like
('all' | 'some' | 'none')
- Whitespace characters between the defined tokens are optional and ignored
Examples:
@Usage("<coin type> <amount>")
@Usage("['all'] ['exact']")
@Usage("[<text...>]")
@Usage("(<targetLanguage> '|' | <sourceLanguage> <targetLanguage>) <text...>")
The values for these non-typed parameters are always String
s unless multiple parameters with the same name have a
value given by the user like with the pattern <foo> <foo>
, in which case the value will be a List<String>
.
Warning: If you for example have
- an optional placeholder followed by an optional literal like in
[<placeholder>] ['literal']
or - alternatively a placeholder or literal like in
(<placeholder> | 'literal')
and a user invokes the command with only the parameter literal
, it could fit in both parameter slots.
You have to decide yourself in which slot it belongs. For cases where the literal parameter can never
be meant for the placeholder, you can use Parameters#fixup(...)
to correct the parameters instance
for the two given parameters.
Semantic Parsing and Validation with Type Conversions
The third is an addendum to the second method described above. The syntax is basically the same. The only difference is,
that a colon (':') followed by a parameter type can optionally be added after a parameter name like for example
<amount:integer>
. Parameters that do not have a type specified, are implicitly of type string
. If a colon is needed
within the actual parameter name, a type has to be specified explicitly, as invalid parameter types are not allowed and
will trigger an error at runtime.
The parameter types can be freely defined by supplying parameter converters to define new types or overwrite built-in ones to have a different behavior.
The built-in types currently available are:
- for all message frameworks
decimal
for a floating point number converted toBigDecimal
number
orinteger
for a natural number converted toBigInteger
string
ortext
for a no-op conversion which can be used if a colon is needed in the parameter name or if simply all parameters should have a type specified for consistency
- for Javacord
user_mention
oruserMention
for a mentioned user converted toUser
role_mention
orroleMention
for a mentioned role converted toRole
channel_mention
orchannelMention
for a mentioned channel converted toChannel
- for JDA
user_mention
oruserMention
for a mentioned user converted toUser
role_mention
orroleMention
for a mentioned role converted toRole
channel_mention
orchannelMention
for a mentioned channel converted toTextChannel
To select the typed parameter parser, add the qualifier @ParameterParser.Typed
to the injected ParameterParser
.
Examples:
@ApplicationScoped
@Usage("<text...>")
class PingCommand implements Command<Message> {
@Inject
ParameterParser parameterParser;
@Override
public void execute(CommandContext<? extends Message> commandContext) {
Message incomingMessage = commandContext.getMessage();
try {
parameterParser.parse(commandContext);
} catch (ParameterParseException ppe) {
incomingMessage.getChannel()
.sendMessage(format("%s: %s", incomingMessage.getAuthor().getDisplayName(), ppe.getMessage()))
.exceptionally(ExceptionLogger.get());
return;
}
incomingMessage.getChannel()
.sendMessage("pong: " + commandContext.getParameterString().orElse(""))
.exceptionally(ExceptionLogger.get());
}
}
@ApplicationScoped
@Usage("[<user:userMention>] ['exact']")
class DoCommand implements Command<Message> {
@Inject
@Typed
ParameterParser parameterParser;
@Override
public void execute(CommandContext<? extends Message> commandContext) {
Parameters<String> parameters;
try {
parameters = parameterParser.parse(commandContext);
} catch (ParameterParseException ppe) {
Message incomingMessage = commandContext.getMessage();
incomingMessage.getChannel()
.sendMessage(format("%s: %s", incomingMessage.getAuthor().getDisplayName(), ppe.getMessage()))
.exceptionally(ExceptionLogger.get());
return;
}
parameters.fixup("user mention", "exact");
boolean exact = parameters.containsParameter("exact");
Optional<String> otherUser = parameters.get("user mention");
// ...
}
}
Customizing Parameter Converters
A custom parameter converter can be configured by providing a CDI bean that implements the
ParameterConverter
interface. The implementation of the convert
method
calculates the converted parameter value using the string parameter, parameter type, and command context.
The class also needs to be annotated with one or multiple ParameterType
qualifiers
that define the parameter type aliases for which the annotated parameter converter works. Without such
qualifier the converter will simply never be used. It is an error to have multiple parameter converters with the
same parameter type that can be applied to the same framework message type, and this will produce an error latest
when a parameter with that type is being converted. The only exceptions are the built-in parameter types.
A user-supplied converter with the same parameter type as a built-in converter will be preferred,
but it would still be an error to have multiple such overrides for the same type.
Examples:
@ApplicationScoped
@ParameterType("strings")
class StringsConverter implements ParameterConverter<Object, List<String>> {
@Override
public List<String> convert(String parameter, String type, CommandContext<?> commandContext) {
return asList(parameter.split(","));
}
}
Customizing the Command Recognition and Resolution Process
The command recognition and resolution process consists of five phases. Actually, these phases can vary, as a command handler or a command context transformer can fast-forward the process to a later phase to skip unnecessary work, or some phase can fail the process with a command not found event.
The five phases that are handled are in order:
- Initialization
- Prefix Computation
- Alias and Parameter String Computation
- Command Computation
- Command Execution
For all but the first and last, there is a before and an after sub phase each during which the command context transformer is called.
If at the end of the initialization phase, or any before / after sub phases, the command is set in the context, processing is fast forwarded immediately to the command execution phase and all other inbetween phases and sub phases are skipped.
If at the end of the initialization phase, or any before / after sub phases before the
BEFORE_COMMAND_COMPUTATION
sub phase, the alias is set in the context, processing is
fast forwarded immediately to the before command computation sub phase and all other inbetween
phases and sub phases are skipped.
If at the end of the initialization phase, or any before / after sub phases before the
BEFORE_ALIAS_AND_PARAMETER_STRING_COMPUTATION
sub phase, the prefix is set in the context,
processing is fast forwarded immediately to the before alias and parameter string computation
sub phase and all other inbetween phases and sub phases are skipped.
Before Prefix Computation Sub Phase
At the start of this sub phase, usually only the message and message content are set.
After Prefix Computation Sub Phase
At the start of this sub phase, usually only the message, message content, and prefix are set.
If at the end of this sub phase no fast forward was done and no prefix is set, a command not found event is being fired and processing stops completely.
Before Alias and Parameter String Computation Sub Phase
At the start of this sub phase, usually only the message, message content, and prefix are set.
If at the end of this sub phase no fast forward was done and no prefix is set, a command not found event is being fired and processing stops completely.
If at the end of this sub phase a prefix is set and it does not match the start of the message content, the message is ignored and processing stops completely. This is the only way to stop processing cleanly without getting a command not found event fired. This can also be achieved by fast forwarding to this phase by setting the prefix in an earlier phase and not doing anything in this phase actually, or not even registering for it.
After Alias and Parameter String Computation Sub Phase
At the start of this phase, usually only the message, message content, prefix, alias, and parameter string are set.
If at the end of this phase no fast forward was done and no alias is set, a command not found event is being fired and processing stops completely.
Before Command Computation Sub Phase
At the start of this phase, usually only the message, message content, prefix, alias, and parameter string are set.
If at the end of this phase no fast forward was done and no alias is set, a command not found event is being fired and processing stops completely.
After Command Computation Sub Phase
At the start of this phase, usually the command context is fully populated.
If at the end of this phase no command is set, a command not found event is being fired and processing stops completely.
Hooking into a Sub Phase
For each of the described sub phases, exactly one context transformer that is compatible with the framework message
can be registered. If you need multiple such transformers, then make one distributing transformer that calls the
other transformers in the intended order. You can also register one context transformer for multiple phases.
The transform
method gets the current phase as argument and can then decide what to do based on that phase parameter.
A command context transformer can be registered by providing a CDI bean that implements the
CommandContextTransformer
interface. In the implementation of the transform
method, given the current phase, you can transform the current command context, or even return a completely new one.
Additionally the bean has to be annotated with at least one @InPhase
qualifier annotation, or it
will simply not be used silently. The transformer will be called for each sub phase that is added with that annotation.
There are also helper classes that can be used as super classes for custom command context transformers, for example, a transformer that returns the mention string for the bot as command prefix if Javacord is used as the underlying message framework.
Warning: The command prefix can technically be configured to be empty, but this means that if the alias and parameter string computation phase is executed, every message will be checked against a regular expression and that for every non-matching message a CDI event will be sent. It is better for the performance if a command prefix is set instead of including it in the aliases directly. Due to this potential performance issue, a warning is logged each time a message is handled with an empty command prefix. If you do not care and want the warning to vanish, you have to configure your logging framework to ignore this warning, as it also costs additional performance and might hide other important log messages. ;-)
Example use-cases for command context transformers are:
-
Custom command prefixes depending on some database
-
Dynamic commands stored in some database
-
Fuzzy-searching for mistyped aliases and their automatic correction (this could also be used for just a "did you mean X" response, but for that the command not found events are maybe better suited)
-
Having a command that forwards to one command in one channel but to another command in another channel, like
!player
that forwards to!mc:player
in an MC channel but to!s4:player
in an S4 channel -
Supporting something like
!runas @other-user foo bar baz
, where the transformer will transform that to aliasfoo
and parameter stringbar baz
, storing theother-user
as additional data in the command context, and then a customRestriction
can check whether the message author has the permissions to use!runas
and for example whether theother-user
would have permissions for thefoo
command and only then allow it to proceed -
forwarding to a
!help
command if an unknown command was issued
Examples:
@ApplicationScoped
@InPhase(BEFORE_PREFIX_COMPUTATION)
class MyPrefixTransformer implements CommandContextTransformer<Message> {
@Override
public <T extends Message> CommandContext<T> transform(CommandContext<T> commandContext, Phase phase) {
Optional<Server> server = commandContext.getMessage().getServer();
if (!server.isPresent()) {
return commandContext.withPrefix("!").build();
} else if (server.get().getId() == 12345L) {
return commandContext.withPrefix("bot ").build();
} else {
return commandContext.withPrefix(":").build();
}
}
}
@ApplicationScoped
@InPhase(BEFORE_PREFIX_COMPUTATION)
class MentionPrefixTransformer extends MentionPrefixTransformerJavacord {
}
@ApplicationScoped
@InPhase(AFTER_ALIAS_AND_PARAMETER_STRING_COMPUTATION)
class MyAliasAndParameterStringTransformer implements CommandContextTransformer<Message> {
@Override
public <T extends Message> CommandContext<T> transform(CommandContext<T> commandContext, Phase phase) {
return (!commandContext.getAlias().isPresent())
? commandContext.withAlias("help").build()
: commandContext;
}
}
Storing Additional Data in the Command Context
The CommandContext
is also a store for arbitrary additional information that you can attach to a command invocation.
You can, for example, attach some information during execution of a command context transformer and then later
evaluate this information in a custom restriction class or in the implementation of the actual command.
The methods regarding additional data all have a generic type argument, as they all return a value, either the current
one or the previous one, depending on the method. This value can be cast to the correct type for you, but be careful
to select the proper type. As this is an unsafe operation, the type has to be chosen wisely. If you, for example, select
String
as the type and then try to get a User
object from the returned optional, you will get a ClassCastException
at runtime.
The type can be specified explicitly like
commandContext.<User>getAdditionalData("user");
or using implicit type inference like with
Optional<User> user = commandContext.getAdditionalData("user");
CDI Events
Handling Missing Commands
If a message starts with the configured command prefix, but does not map to an available command, the command handlers send an async CDI event that you can observe and handle to react accordingly like sending a message that a command was not found.
Example:
@ApplicationScoped
class EventObserver {
void commandNotFound(@ObservesAsync CommandNotFoundEventJavacord commandNotFoundEvent) {
commandNotFoundEvent.getMessage()
.getChannel()
.sendMessage(format(
"Command %s%s was not found!",
commandNotFoundEvent.getPrefix(),
commandNotFoundEvent.getUsedAlias()))
.exceptionally(ExceptionLogger.get());
}
}
Handling Disallowed Commands
If a command was found but not allowed by some restriction rules, the command handlers send an async CDI event that you can observe and handle to react accordingly like sending a message that a command was not allowed.
Example:
@ApplicationScoped
class EventObserver {
void commandNotAllowed(@ObservesAsync CommandNotAllowedEventJavacord commandNotAllowedEvent) {
commandNotAllowedEvent.getMessage()
.getChannel()
.sendMessage(format(
"Command %s%s was not allowed!",
commandNotAllowedEvent.getPrefix(),
commandNotAllowedEvent.getUsedAlias()))
.exceptionally(ExceptionLogger.get());
}
}
Getting the Library Version Programmatically
You are welcome to mention in some "about" command or similar that you use this library to attract other people to use
it too. If you want to also mention which version you are using, you can use the getDisplayVersion
method of the
Version
CDI bean by injecting it into your code.
Supporting other Message Frameworks
If you want to support a message framework that is not natively supported already, you need to provide a CDI bean that
extends the CommandHandler
class. You are also welcome to contribute back any such
implementation to the library for all users benefit. You should read the JavaDoc of the CommandHandler
class and have
a look at any of the existing implementations to get started with writing your own implementation. Most of the common
logic should be done in the CommandHandler
class already and just some framework-dependent things like attaching
message listeners to the underlying framework need to be done in the subclass.
Version Numbers
Versioning of this library follows the Semantic Versioning specification.
But only classes in packages starting with net.kautler.command.api
are bound to the backwards compatibility
constraints of semantic versioning. All other classes are considered internal and can have breaking changes anytime.
License
Copyright 2019-2022 Björn Kautler
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.