Skip to main content

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#

note

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 of Presence
  • Use ClientActivity instead of Activity
  • Prefer calling setInitialPresence over setInitialStatus
  • 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.

warning

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

Renamed interaction types#

The following versions are affected and need to migrate to these types when upgrading:

  • v3.1.7
  • v3.2.0-RC3
PreviousNew
SlashCommandEventChatInputInteractionEvent
ComponentInteractEventComponentInteractionEvent
ButtonInteractEventButtonInteractionEvent
SelectMenuInteractEventSelectMenuInteractionEvent

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 ChatInputInteractionEventevent.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 instanceStore 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.