While V3 has most major features reworked and improved, this came to a cost; in particular, some parts of the API are now built toward Kotlin users. It is not sure how the API may be adapted to fit Java users more.
While some features that require configuration using a DSL appeal more to Kotlin users, they should still be usable by Java users.
Fortunately, annotation-driven features that already existed can still be used with no problem, both in Java and Kotlin.
You can also refer to the examples and bot templates (Java / Kotlin) to have an idea on how V3 is supposed to be used.
To better align with the Maven coordinates, the base package has changed from com.freya02
to io.github.freya022
.
Fixing this should be as simple as using Find & Replace.
All commands and handlers support coroutines (except service factories) and default parameters.
Each feature has its own CoroutineScope
, configurable in BCoroutineScopesConfigBuilder
In V2, you had to register your resolvers, instances, instance suppliers... using ExtensionsBuilder
, which enabled you to use constructor and field injection.
In V3, you no longer need to manually manage your instances manually, a basic DI framework can manage all your services, commands, handlers, resolvers... as long as you annotated them with their appropriate annotations.
You then simply put a parameter in your constructor/method to receive the requested service type.
@BService
, @Command
and @Resolver
are the annotations you will need the most.
Spring IoC is also supported.
@BService
can be used to declare classes as services, or, when used on a method, serves as a factory for the return type.
As always, if they are on your search path, then they will be instantiated when building the framework,
and available to other classes.
These services can all have a name, in case you want multiple services of the same type, but want to differentiate them.
When requesting a service, only one service must correspond to the given type/name.
If multiple service providers exist, only one must be usable, or, only one must be marked primary (using @Primary
).
Note: Service factories are prioritized over annotated classes.
Kotlin note: Service factories can be top-level functions.
@ConditionalService
can be used when you want your service/command/resolver... to be created under certain conditions.
The annotation will only let the service be created if all the specified check interfaces passes.
@Condition
is a meta-annotation that can be used on an annotation you create,
which enables this annotation to represent a custom condition for service creation.
You can also specify if failure of these annotations throws an exception.
An example can be found here.
You can find the @InterfacedService
annotations on some interfaces of the framework, such as SettingsProvider
or IHelpCommand
.
This annotation indicates that this interface can be implemented, but needs to be registered as a service.
This is useful for when the framework needs an instance of the interface, without knowing what the implementation is.
For example, if you want to override the help command, you will need to make an implementation for IHelpCommand
.
Most interfaces that were configurable in CommandsBuilder
were replaced by interfaced services,
such as command/component filters, SettingsProvider
, ExceptionHandler
, AutocompleteTransformer
, etc...
An example can be found here.
Dynamic suppliers are interfaced services (that you can make multiple instances of),
which lets you provide services of any type.
The framework will give you the class of what it wants to instantiate,
and you can then tell if your supplier supports the class, or not,
or if it is supported but cannot create an instance of it.
An example can be found here.
Annotated text and slash commands suffered from several issues, due to the data either being applied to the wrong part of the command, or no data being applicable on a part of a command (such as subcommand group descriptions).
To reduce confusion about which property applies to what, and reduce undefined behaviors, annotations with distinct scopes were introduced:
Slash commands:
- (Required)
@JDASlashCommand(name, group, subcommand, description)
- (Required once, when using subcommands)
@TopLevelSlashCommandData(scope, defaultLocked, nsfw, description)
- (Optional)
@SlashCommandGroupData(description)
Text commands:
- (Required)
@JDATextCommandVariation(path, order, description, usage, example)
- (Optional)
@TextCommandData(path, aliases, description)
, this must be used alongside@JDATextCommandVariation
,path
is the path this annotation applies to, defaults to the variation's path
This way ensures that the data is only applied to a specific command, and does not need to be repeated, or follow specific orders.
More details on the PR.
Commands can now be declared using a DSL, these works best if you use Kotlin.
To use the DSL, you can check out GlobalApplicationCommandProvider
/GuildApplicationCommandProvider
and TextCommandProvider
.
The DSL can help you provide each parameter explicitly, without the need for annotations, and also enables more features, such as option aggregates.
The DSL also enables you to declare commands with code, configure your names, descriptions, choices... everything by code, so you are not limited to static values with annotations.
You can find an example here,
see SlashBanDetailedFront#declareGlobalApplicationCommands
.
Option aggregates are a way to combine multiple options into one object, the function that combines all the options can be anything, including a constructor.
Normal aggregates can accept any option type (Discord option, custom option or generated option).
You can still insert options without declaring an aggregate; these options will implicitly have an aggregate created for you.
Note: Option aggregates are only available with DSL declaration (and components and modal handlers by using @Aggregate
).
You can find an example here,
see aggregate
in SlashBanDetailedFront#declareGlobalApplicationCommands
.
Vararg options are a special type of option aggregate, they are essentially an aggregate that generates N options,
and the aggregator just accepts a List
and returns it as-is, i.e. your parameter accepts a List
, not a real vararg.
You can use these with optionVararg
.
Note: Aggregators can accept List
parameters, but all the options must be under the same declared parameter name, so they can be all put in the list.
You can find an example here.
Kotlin's inline classes can also be used as options,
you can use inlineClassOption
to declare one in the DSL, they also automatically work for annotated commands.
inlineClassOptionVararg
can also be used for inline classes that accept a varargs.
You can find an example here,
with SlashSentence.SentenceParts
.
A token bucket-based rate limiting has been added,
while @Cooldown
still exists, @RateLimit
now lets you define buckets, with multiple bandwidths,
letting you create custom rate limiting for each of your command/component handler.
A common example is a spike protected bucket, which, in addition to the normal rate limit, helps you prevent users from spamming a command in a short period of time, forcing them to spread out your resource usage.
Rate limits can be used with:
@RateLimit
(for annotated commands)#rateLimit
/#cooldown
(for DSL commands)#rateLimitReference
(for DSL commands and components)- A provider, by implementing
RateLimitProvider
and usingRateLimitManager#rateLimit
, using them is as simple as using@RateLimitReference
for annotated commands, or#rateLimitReference
for DSL commands and components.
A bucket token can be added back by using your event's #cancelRateLimit()
,
which effectively cancels the rate limit applied before entering any handler.
In addition to being implemented as services, filters can now be used on specific commands / components.
These filters can be declared as global filters, but must override the global
property with false
,
you can then reference those filters in:
@Filter
: Requires all filters to pass, can only be used on commands- The
filters
property of command DSLs, you can even combine filters usingand
andor
, such asfilters += (filter<IsBotOwner>() or filter<IsGuildOwner>()) and filter<InVoiceChannel>()
Component filters must be passed while building the components, and have the same usage as for command DSLs.
Java users can also use the addFilter
methods.
Autocomplete annotations and event have been renamed using Autocomplete
instead of Autocompletion
.
Other than that, @CompositeKey
has been replaced by compositeKeys
on @CacheAutocomplete
.
This lets you configure what Discord options (you can also put the parameter name) must be in the caching key,
even if the option is not being used by the autocomplete handler itself.
The slash command DSL also let you configure autocomplete by using SlashCommandOptionBuilder#autocomplete
(or SlashCommandOptionBuilder#autocompleteReference
for handlers defined by annotation).
You can find an example here,
on SlashSentence#onSentencePartAutocomplete
.
Text commands no longer have a name
/group
/subcommand
, they have a path
instead, which is an array of string.
Note: Text commands are still limited to three path parts.
The command-specific embed has been revamped, and has separated descriptions for the command and the variations themselves.
You can also add per-variant usage and examples, both in annotations and in the DSLs.
Example
@Command
class TextBan : TextCommand() {
// Applies to "ban"
@TextCommandData(description = "Permanently bans an user.")
@JDATextCommandVariation(path = ["ban"])
suspend fun onTextBan(event: BaseCommandEvent, @TextOption user: InputUser) { ... }
@JDATextCommandVariation(path = ["ban"])
suspend fun onTextBan(event: BaseCommandEvent, @TextOption(example = "freya02") name: String) { ... }
// Applies to "temp" of "ban"
@TextCommandData(description = "Temporarily bans an user.")
@JDATextCommandVariation(path = ["ban", "temp"])
suspend fun onTextBanTemp(event: BaseCommandEvent, @TextOption user: InputUser) { ... }
@JDATextCommandVariation(path = ["ban", "temp"])
suspend fun onTextBanTemp(event: BaseCommandEvent, @TextOption(example = "freya02") name: String) { ... }
}
While V2 had to wait for your entire bot to be loaded, V3 requires you to start the framework before building JDA, which lets you get your stuff started up before the bot goes fully online.
Building JDA before the framework will result in an error, I strongly recommend that you use a service which implements JDAService
.
You can also refer to the example JDA service.
A Database
service has been added,
helping you get a decent abstraction for transactions and reading result rows with ease.
BlockingDatabase
is the equivalent for Java users, with the same features.
While PostgreSQL is still strongly recommended, H2 is also supported, but requires the PostgreSQL compatibility mode.
This allows you to run an in-memory database, or have it saved to a file,
see ConnectionSupplier
and H2 Database connection modes for more details.
This service helps you create statements that are logged at the class that created the statement. That way, you can enable traces of some of your classes, without having everything else logged.
The logged statements are reconstructed from the parametrized SQL, and then filled with the parameters, giving you an executable query, which can be run in a console.
See Database
for more details.
Additionally, if BDatabaseConfig#dumpLongTransactions
is enabled,
the framework will report transactions taking longer than ConnectionSupplier#maxTransactionDuration
,
and will trigger a coroutine / thread dump.
Note: Coroutine dumps require kotlinx-coroutines-debug
, see BDatabaseConfig#dumpLongTransactions
for more details.
The Components
utility class is now a service, which means you can get it either in your constructor, or in your handler.
Like in V2, your components can be set as being usable only once, being able to be used by certain users/with permissions/with roles... They can also be constructed using the builder pattern.
Kotlin users can configure their components using a DSL, as well as having the handler, and the timeout handler be optional.
As these handlers are optional, you can still handle them using coroutines, by using await
on your component/group.
Note: Components have a default timeout (set in Components
), which invalidates the button after expiration.
You can disable the timeout if necessary using noTimeout()
, or if you plan on putting the component in a group.
Kotlin users need to make sure to catch TimeoutCancellationException
when using await()
on them.
An example can be found here.
Just like components, modals are now created using a DSL, while their handlers are still annotated.
The DSL is very similar to the component's DSL, with your usual bindTo
and setTimeout
functions, you can also await on your modals.
Note: Modals also have a default timeout (set in Modals
), which invalidates the modal after expiration.
You can disable the timeout if necessary using noTimeout()
.
Kotlin users need to make sure to catch TimeoutCancellationException
when using await()
on them.
You can find an example here.
@BEventListener
can now be specified to be run asynchronously (within the parallelism limits of BCoroutineScopesConfig#eventDispatcherScope
),
they can also have a priority assigned to them, as well as a timeout, used for suspending handlers.
An example can be found here.
ParameterResolver
is now type safe and also supports coroutines.
Parameter resolvers in Java can override the usual resolve
method, while Kotlin users can override the resolveSuspend
function. This is particularly useful when you need to execute a RestAction
, so you can await without blocking the thread.
Resolver factories were also added to enable you to give a parameter resolver based on the parameter of the function.
That way, you can return resolvers based on your own conditions, for example, given the parameter's annotations or generics.
This is how [App/Text]LocalizationContext
are injected, they use factories of ICustomResolver
,
and when you put a parameter, they read that parameter for @LocalizationBundle
and then construct a resolver which gets you the correct localization bundle.
Resolvers can not only be created from classes, but also from service factories (function with @BService
)
returning an implementation.
For example, the framework provides Resolver#enumResolver
, which can help you quickly handle any enumeration,
while also (optionally) letting you transform a value into its displayed string.
The API has been improved to allow a more detailed loading mechanism, as to let you extend the API, such as adding support for new formats (like HOCON), or new file structures:
Name | Function |
---|---|
LocalizationMapProvider | Responsible for getting a LocalizationMap for a given base name and locale, using a broader locale is allowed. The provider can merge multiple maps as well as modify the base name. All providers can be found by using LocalizationService#getMappingProviders . |
LocalizationMapReader | Responsible for reading a given localization map for a given bundle name (base name + locale). It is allowed to create any type of LocalizationTemplate , but is not allowed to use a different name or locale. All readers can be found by using LocalizationService#getMappingReaders . |
LocalizationMapTemplate | Represents a localization template, i.e., the entire string with parameters in it. |
FormattableArgumentFactory | Responsible for creating FormattableArgument s based on an argument found in a template. All factories can be found by using LocalizationService#getFormattableArgumentFactories . |
The old Localization
factory is now LocalizationService
.
As briefly explained above, localization has been moved from the framework events, into injectable instances, [App/Text]LocalizationContext
.
Naturally, AppLocalizationContext
can only be used with interactions (application commands, components, modals), while TextLocalizationContext
can be used in interactions and MessageReceivedEvent
handlers.
These parameters must be annotated with @LocalizationBundle
, as to specify where to take the translations from,
and optionally, with what prefix.
You can specify as many of them as you'd like, as well as construct them manually using the static methods,
or use switchBundle
which changes the target bundle and clears the prefix.
In addition, #localize[X]orNull
can help you return null
in case the given localization path does not exist.
You can find an example here.
All paginators were reworked with several improvements, here is a list of the changes:
ButtonMenu
->ChoiceMenu
InteractiveMenu
->NestedPagination
- A
Paginators
service that serves as a pagination factory has to be used, improving discoverability - Mandatory parameters are no longer behind setters, they are requested by the factory
ButtonMenu
can now change the button styleButtonMenu
can have its buttons reused- Added support for Kotlin coroutines and
Duration
- Some callbacks (like
PageEditor
) cannot use coroutines as they are called by method overridable by Java users
- Some callbacks (like
NestedPagination
saves the page number before switching
- Most options use default values, such as paginator buttons and timeouts
- Requesting a new page invalidates the replaced page's components, can be disabled
- Paginators expire by default using the same timeout as in
Components
- Timeout consumers are optional, timeout can be entirely disabled
- Page editors no longer require returning an embed, you can freely edit the message and/or the existing embed
- Builders are passed instead of their individual values
- Creating a message from the pagination's state is fully handled by the base class, and decomposed into steps that can be overridden
Filters now support coroutines and are run right before their target should have been executed.
Several top-level and extension functions have been added, such as:
- Member retrieval, temporary message content warning suppressing, message/hook send/edit/replaceWith, overloads using Kotlin's Duration
- Error response handling/ignoring
- Resource reading, named coroutine scopes
A InputUser
interface, extending User
, provides a way for you to get a Member (null if not available)
alongside the non-null User, without retrieving.
This is particularly useful for commands that work for both User
s and Member
s,
but where having a Member
triggers additional checks, such as in ban commands.
emoji-java has been replaced with JEmoji,
providing a more up-to-date emoji list,
also allowing dropping org.json
and fixing issues with emoji indexes, and incorrect fitzpatrick formats.