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.
- Prerequisites
- Supported Message Frameworks
- Setup
- Usage
- Version Numbers
- License
- Java 8+
- One of the supported message frameworks
- An implementation of CDI that implements CDI 2.0 like Weld SE
- [Optional] ANTLR runtime 4.7.2 if the
ParameterParseris used
The following message frameworks are currently supported:
If you want to have support for an additional, do not hesitate to open a PR or feature request issue.
repositories { mavenCentral() }
dependencies { implementation 'net.kautler:command-framework:0.3.0' }<dependency>
<groupId>net.kautler</groupId>
<artifactId>command-framework</artifactId>
<version>0.3.0</version>
</dependency>Download the JAR for the latest release from the Latest Release Page and include it in your project.
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
public class JavacordProducer {
@Inject
private Logger logger;
@Inject
@Named
private String discordToken;
@Produces
@ApplicationScoped
private DiscordApi produceDiscordApi() {
return new DiscordApiBuilder()
.setToken(discordToken)
.login()
.whenComplete((discordApi, throwable) -> {
if (throwable != null) {
logger.error("Exception while logging in to Discord", throwable);
}
})
.join();
}
private void disposeDiscordApi(@Disposes DiscordApi discordApi) {
discordApi.disconnect();
}
}Tested versions:
3.0.5
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
public class JdaProducer {
@Inject
private Logger logger;
@Inject
@Named
private String discordToken;
@Produces
@ApplicationScoped
private JDA produceJda() {
try {
return new JDABuilder(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.0.0_52
Create a CDI bean that implements the Command interface.
Example:
@ApplicationScoped
public class PingCommand implements Command<Message> {
@Override
public void execute(Message incomingMessage, String prefix, String usedAlias, String parameterString) {
incomingMessage.getChannel()
.sendMessage("pong: " + parameterString)
.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 according methods in your command implementation to replace the default implementation which evaluates the annotations. Having annotations applied and at the same time overwriting the according methods makes only sense if you want the annotations only for documentary purpose or evaluate them yourself, as the default implementations of those methods are the only places where the annotations are evaluated by default. Every other place like for example command handler call the methods on the command implementation.
By overwriting the Command#getAliases() method or applying one or multiple @Alias annotations the
aliases to which the command reacts can be configured. If at least one alias is configured, only the explicitly
configured ones are available. If no alias is configured, the class name, stripped by Command or Cmd
suffix and / or prefix and the first letter lowercased is used as default.
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 execute 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. As long as a command is not doing long-running or blocking operations it might be a good idea to not execute the command asynchronously. But if long-running or blocking operations are done in the command code directly, depending on the underlying message framework it might be a good idea to execute the command asynchronously to not block message dispatching which could introduce 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.
By overwriting the Command#getDescription() method or applying the @Description annotation
the description of the command can be configured. Currently this description is used nowhere, but can for example be
displayed in an own help command.
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 in an all-of, any-of, or none-of logic.
For more complex boolean logic either overwrite the getRestrictionChain method or provide own 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
public class Vampire extends UserJavacord {
private Vampire() {
super(341505207341023233L);
}
}@ApplicationScoped
public class MyFancyServer extends ServerJavacord {
@Inject
private MyFancyServer(@Named("myFancyServerId") long myFancyServerId) {
super(myFancyServerId);
}
}By overwriting the Command#getUsage() method or applying the @Usage annotation the usage of the
command can be configured. This usage can for example be displayed in an own help command.
When using the ParameterParser, the usage string has to follow a pre-defined format that
is described at Parsing Parameters.
There are two helpers to split the parameterString that is provided to Command#execute(...) into multiple parameters
that can then be handled separately.
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 whether too many parameters were given to the command.
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, an IllegalArgumentException is thrown that can be caught and
reported to the user giving wrong arguments. The exception message is suitable to be directly forwarded to user.
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...>")}
Warning: If you have an optional literal parameter following an optional placeholder parameter like for example
[<user mention>] ['exact'] and a user invokes the command with only the parameter exact, 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 ParameterParser#fixupParsedParameter(...) to correct the parameters map for
the two given parameters.
Examples:
@ApplicationScoped
@Usage("<text...>")
public class PingCommand implements Command<Message> {
@Inject
private ParameterParser parameterParser;
@Override
public void execute(Message incomingMessage, String prefix, String usedAlias, String parameterString) {
try {
parameterParser.getParsedParameters(this, prefix, usedAlias, parameterString);
} catch (IllegalArgumentException e) {
incomingMessage.getChannel()
.sendMessage(format("%s: %s", incomingMessage.getAuthor().getDisplayName(), e.getMessage()))
.exceptionally(ExceptionLogger.get());
return;
}
incomingMessage.getChannel()
.sendMessage("pong: " + parameterString)
.exceptionally(ExceptionLogger.get());
}
}@ApplicationScoped
@Usage("[<user mention>] ['exact']")
public class DoCommand implements Command<Message> {
@Inject
private ParameterParser parameterParser;
@Override
public void execute(Message incomingMessage, String prefix, String usedAlias, String parameterString) {
Map<String, String> parameters;
try {
parameters = parameterParser.getParsedParameters(this, prefix, usedAlias, parameterString);
} catch (IllegalArgumentException e) {
incomingMessage.getChannel()
.sendMessage(format("%s: %s", incomingMessage.getAuthor().getDisplayName(), e.getMessage()))
.exceptionally(ExceptionLogger.get());
return;
}
parameterParser.fixupParsedParameter(parameters, "user mention", "exact");
boolean exact = parameters.containsKey("exact");
boolean otherUserGiven = parameters.containsKey("user mention");
String otherUserMention = parameters.get("user mention");
// ...
}
}A custom command prefix can be configured by providing a CDI bean that implements the
PrefixProvider interface. In the implementation of the getCommandPrefix method you can
determine from the message that caused the command what the command prefix should be. This way you can for example have
different command prefixes for different servers your bot is serving. If no custom prefix provider is found, a default
one is used, that always returns "!" as the prefix.
There are also helper classes that can be used as super classes for own prefix providers like for example a provider 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 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. ;-)
Examples:
@ApplicationScoped
public class MyPrefixProvider implements PrefixProvider<Message> {
@Override
public String getCommandPrefix(Message message) {
Optional<Server> server = message.getServer();
if (!server.isPresent()) {
return "!";
} else if (server.get().getId() == 12345L) {
return "bot ";
} else {
return ":";
}
}
}@ApplicationScoped
public class MentionPrefixProvider extends MentionPrefixProviderJavacord {
}The alias calculation can be customized by providing a CDI bean that implements the
AliasAndParameterStringTransformer interface. In the implementation of
the transformAliasAndParameterString method you can determine from the message that caused the processing what the
alias and parameter string should be. The transformer is called after the alias and parameter string are determined from
the message using all registered aliases and before the command is resolved from the alias. If an alias was found from
the registered aliases, the aliasAndParameterString parameter contains the found information. If no alias was found,
the parameter will be null. The fields in the AliasAndParameterString object are always non-null.
The transformer can then either accept the found alias and parameter string by returning the argument directly, or it
can determine a new alias and parameter string and return these. The return value of the transformer will be used for
further processing. If the alias in the returned object is not one of the registered aliases or the transformer returns
null, there will not be any command found and the respective CDI event will be fired.
Example use-cases for this are:
-
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 probably better suited)
-
having a command that forwards to one command in one channel but to another command in another channel, like
!playerthat forwards to!mc:playerin an MC channel but to!s4:playerin an S4 channel -
supporting something like
!runas @other-user foo bar baz, where the transformer will transform that to aliasfooand parameter stringbar bazand then a customRestrictioncan check whether the message author has the permissions to use!runasand then for example whether theother-userwould have permissions for thefoocommand and only then allow it to proceed -
forwarding to a
!helpcommand if an unknown command was issued
Example:
@ApplicationScoped
public class MyAliasAndParameterStringTransformer implements AliasAndParameterStringTransformer<Message> {
@Override
public AliasAndParameterString transformAliasAndParameterString(
Message message, AliasAndParameterString aliasAndParameterString) {
return (aliasAndParameterString == null)
? new AliasAndParameterString("help", "")
: aliasAndParameterString;
}
}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
public class EventObserver {
private void commandNotFound(@ObservesAsync CommandNotFoundEventJavacord commandNotFoundEvent) {
commandNotFoundEvent.getMessage()
.getChannel()
.sendMessage(format(
"Command %s%s was not found!",
commandNotFoundEvent.getPrefix(),
commandNotFoundEvent.getUsedAlias()))
.exceptionally(ExceptionLogger.get());
}
}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
public class EventObserver {
private void commandNotAllowed(@ObservesAsync CommandNotAllowedEventJavacord commandNotAllowedEvent) {
commandNotAllowedEvent.getMessage()
.getChannel()
.sendMessage(format(
"Command %s%s was not allowed!",
commandNotAllowedEvent.getPrefix(),
commandNotAllowedEvent.getUsedAlias()))
.exceptionally(ExceptionLogger.get());
}
}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.
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 sub-class.
Versioning of this library follows the Semantic Versioning specification.
Copyright 2019 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.