Migrating from v3.1 to v3.2
This article should help you update your bot from Discord4J v3.1.x to v3.2.0. This new major release is over a year in the making and has lots of changes.
Before you start
If you encounter an issue while following this guide or discover something missing, feel free to suggest changes or discuss them in our server. Thanks!
Updating dependencies
Discord4J v3.2 depends on Reactor 2020 release train (Reactor Core 3.4.x and Reactor Netty 1.0.x). It maintains the JDK 8 baseline and includes other dependency upgrades like:
- discord-json 1.6 (from 1.5)
- jackson-databind 2.12 (from 2.11)
- caffeine 2.8 (new dependency)
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'com.discord4j:discord4j-core:3.2.0'
}
Maven
<dependencies>
<dependency>
<groupId>com.discord4j</groupId>
<artifactId>discord4j-core</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
Discord4J features
Gateway Intents
The intent system is now mandatory in the Gateway version used with v3.2. To retain the previous behavior, use setEnabledIntents(IntentSet.all())
when building a Gateway-capable client. For more information about this feature check official docs.
GatewayDiscordClient client = DiscordClient.create(System.getenv("token"))
.gateway()
.setEnabledIntents(IntentSet.all())
.login()
.block();
Otherwise the default will be IntentSet.nonPrivileged()
. Discord might be changing the set of non-privileged intents in the future, particularly message create becoming privileged in 2022.
If you get an error such as WebSocket closed: 4014 Disallowed intent(s)
, make sure you're allowed to use the intents you enabled, this can be done in your developer portal bot page, look for Privileged Gateway Intents.
New entity cache API
One of the most notable change is the way our Store
abstraction works for entity caching. If you use a custom StoreService
, for quick migration you need to adapt it in this way:
GatewayDiscordClient client = DiscordClient.create(System.getenv("token"))
.gateway()
.setStore(Store.fromLayout(LegacyStoreLayout.of(myStoreService)))
.login()
.block();
Where myStoreService
is what you used previously, for instance:
StoreService myStoreService = MappingStoreService.create()
.setMapping(new NoOpStoreService(), MessageData.class)
.setFallback(new JdkStoreService());
New bot presence API
After #874, you have to update how bot presence is set when connecting and updated:
- Use
ClientPresence
instead ofPresence
- Use
ClientActivity
instead ofActivity
- Prefer calling
setInitialPresence
oversetInitialStatus
- This also affects methods like
GatewayDiscordClient::updatePresence
DiscordClient.create(System.getenv("token"))
.gateway()
.setInitialPresence(s -> ClientPresence.invisible())
.withGateway(client -> client.on(ReadyEvent.class)
.doOnNext(ready -> log.info("Logged in as {}", ready.getSelf().getUsername()))
.then())
.block();
New request spec API
A large effort was introduced in #927 that provides different patterns of building and executing API requests to Discord. This addresses some issues of the Consumer
-based specs when used for templating, and allows more fluent calls for convenience.
Consider the following example of spec usage in 3.1.x:
channel.createMessage(msg -> {
msg.setContent("Hello @everyone");
msg.setAllowedMentions(AllowedMentions.suppressEveryone());
msg.addEmbed(embed -> {
embed.setTitle("Foo");
embed.addField("Bar", "Baz", false);
embed.setColor(Color.BLUE);
});
})
The createMessage
method takes a Consumer<MessageCreateSpec>
. In other words, the spec is "given to you" by the
method, and you mutate that spec to put it in the state you want. While it works well, this pattern was often confusing
to users, most notably because you couldn't ever really "hold" a spec. It was a kind of ephemeral thing that could only
be given to you in these mutating Consumers.
In 3.2, we sought to "materialize" specs. Specs are now immutable data carriers that can be built in a few different ways. The way you pick is purely up to your preference.
Builder
The most obvious way to build an object in Java is the builder pattern. This works exactly how you think it would.
channel.createMessage(MessageCreateSpec.builder()
.content("Hello @everyone")
.allowedMentions(AllowedMentions.suppressEveryone())
.addEmbed(EmbedCreateSpec.builder()
.title("Foo")
.addField("Bar", "Baz", false)
.color(Color.BLUE)
.build())
.build())
Any spec, SomeSpec
, has a static method SomeSpec.builder()
which will return a SomeSpec.Builder
.
Withers
In addition to a builder, all specs come equipped with "wither" (or withX
) methods that return a copy of the current
spec with a modified field. The above example could be equivalently written as...
channel.createMessage(MessageCreateSpec.create()
.withContent("Hello @everyone")
.withAllowedMentions(AllowedMentions.suppressEveryone())
.withEmbeds(EmbedCreateSpec.create()
.withTitle("Foo")
.withFields(EmbedCreateFields.Field.of("Bar", "Baz", false))
.withColor(Color.BLUE)))
Similar to builder()
, specs also have a static create()
method that returns a minimal, default spec which can be
modified using the wither methods.
Fluent Publishers
Finally, most methods that accept specs also have a parameter-less (or minimal parameters) overload that instead returns
a special Mono
or Flux
. These publishers have methods corresponding to each property of the spec. This allows for
fluent calls to these methods.
channel.createMessage("Hello @everyone")
.withAllowedMentions(AllowedMentions.suppressEveryone())
.withEmbeds(EmbedCreateSpec.create()
.withTitle("Foo")
.withFields(EmbedCreateFields.Field.of("Bar", "Baz", false))
.withColor(Color.BLUE)))
For a supported spec, SomeActionSpec
, there will be a corresponding publisher SomeAction(Mono/Flux)
. Note that this
isn't supported for all specs. As you can see above, the parameter to withEmbeds
isn't included in the surrounding
fluent chain (and we could have chosen to use a builder there if we wanted).
Legacy Specs
To aide migration, we will continue to support the previous spec behavior in 3.2. The old ("legacy") specs have been
moved to discord4j.core.spec.legacy
, and had Legacy
prepended to their names. For example, 3.1's
discord4j.core.spec.MessageCreateSpec
is now discord4j.core.spec.legacy.LegacyMessageCreateSpec
in 3.2.
This means that any code using specs like foo(spec -> ...)
will continue to work without issues, but if you directly
imported a spec class, you will need to update the package, as it was moved to discord4j.core.spec.legacy
package and
all XxSpec
classes were deprecated and renamed to LegacyXxSpec
.
These legacy specs are deprecated. They exist only to make migration a bit easier, and they will be removed in a future version. Please let us know if the current alternatives are not a good fit for your use case.
Interactions
This feature is under development from Discord therefore we have marked it as Experimental, meaning breaking changes can happen between minor versions (in D4J that is from x.y.z to x.y.z+1). A new hierarchy was introduced after the inclusion of context menus in #1001
- Event
- InteractionCreateEvent
- ApplicationCommandInteractionEvent
- ChatInputInteractionEvent
- MessageInteractionEvent
- UserInteractionEvent
- ComponentInteractionEvent
- ButtonInteractionEvent
- SelectMenuInteractionEvent
- ApplicationCommandInteractionEvent
- InteractionCreateEvent
Renamed interaction types
The following versions are affected and need to migrate to these types when upgrading:
- v3.1.7
- v3.2.0-RC3
Previous | New |
---|---|
SlashCommandEvent | ChatInputInteractionEvent |
ComponentInteractEvent | ComponentInteractionEvent |
ButtonInteractEvent | ButtonInteractionEvent |
SelectMenuInteractEvent | SelectMenuInteractionEvent |
Webhook execution
Interactions use the webhook execution feature under the hood that was backported from v3.2 to v3.1. In particular, if
you used WebhookMultipartRequest
for your slash command application in v3.1 you now have to migrate to
MultipartRequest<WebhookExecuteRequest>
:
WebhookMultipartRequest request = new WebhookMultipartRequest(body);
Becomes one of these, depending on whether you include attachments to it or not:
MultipartRequest<WebhookExecuteRequest> request = MultipartRequest.ofRequest(body);
MultipartRequest<WebhookExecuteRequest> request = MultipartRequest.ofRequestAndFiles(body, files);
However, starting from v3.2.0, it's less likely you need to directly call MultipartRequest
for interactions, as there
are discord4j-core
methods available now, including the richer and fluent new request spec API:
List<Tuple2<String, InputStream>> file = Collections.singletonList(Tuples.of("myAttachment.zip", inputStream));
Mono<?> edit = press.getInteractionResponse()
.editInitialResponse(MultipartRequest.ofRequestAndFiles(
WebhookMessageEditRequest.builder()
.contentOrNull("Wow, a new attachment!")
.components(Collections.singletonList(row.getData()))
.build(), file));
return press.acknowledge().then(edit);
Can now be migrated to new methods without relying on getInteractionResponse()
:
MessageCreateFields.File file = MessageCreateFields.File.of("myAttachment.zip", inputStream);
Mono<?> edit = press.editReply()
.withContentOrNull("Wow, a new attachment!")
.withFiles(file)
.withComponents(row);
return press.deferEdit().then(edit);
This also applies to event.getInteractionResponse().createFollowupResponse(...)
that is now available at
event.createFollowup()
using the new APIs. For a summary of methods available under this new API,
see this section.
To see the changes in action, check our Examples page.
Ephemeral variants
For example, to reply ephemerally to a command, the below code can now be used:
// event is a ChatInputInteractionEvent
event.reply("Content!")
.withEphemeral(true)
.subscribe();
This is now recommended over calling *Ephemeral
methods.
For more information regarding how to respond to slash commands with the new method, see Application Commands - Responding
Advanced features
Directly querying a store
The recommended way is using EntityRetrievalStrategy.STORE
for methods that support it. However if it's not available for your use case and used StateView
before, you now have to migrate to querying a Store
directly, available from GatewayResources
.
This abstraction is now focused on query objects, named ReadActions
. A quick example to get the cached user count:
// client is a GatewayDiscordClient instance
Store store = client.getGatewayResources().getStore();
long userCount = Mono.from(store.execute(ReadActions.countUsers())).block();
Customizing a StoreLayout
The entity cache from v3.1 set a structure that's too rigid for some implementations, so a new interface StoreLayout
was created to abstract the read/write process that bots needs to handle when connecting to the Discord Gateway to maintain a cache. v3.2 now defaults to Store.fromLayout(LocalStoreLayout.create())
as the new in-memory entity cache.
A LocalStoreLayout
configures a set of defaults as well through StorageConfig
:
- Since messages have a larger storage footprint, a Caffeine cache
StorageBackend
is set to 1000 of the most recent messages - Set to remove all content under stale cache conditions like a non-resumable reconnect or logout
These options can be configured through a builder using: LocalStoreLayout.create(StorageConfig.builder().build())
For an example implementation beyond what's built-in, check this project from Discord4J contributor @napstr: https://github.com/CapybaraLabs/d4j-postgres-store
Other features
Check more details about the API and behavior changes in What's new in v3.2.