diff --git a/application/build.gradle b/application/build.gradle
index 3e9ece84ca..14cd60d0b5 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -45,7 +45,7 @@ dependencies {
     implementation project(':database')
     implementation project(':utils')
 
-    implementation 'net.dv8tion:JDA:5.0.0-alpha.9'
+    implementation 'net.dv8tion:JDA:5.0.0-alpha.20'
 
     implementation 'org.apache.logging.log4j:log4j-core:2.19.0'
     runtimeOnly 'org.apache.logging.log4j:log4j-slf4j18-impl:2.18.0'
diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java
index 5927afe8af..71128aef44 100644
--- a/application/src/main/java/org/togetherjava/tjbot/Application.java
+++ b/application/src/main/java/org/togetherjava/tjbot/Application.java
@@ -3,6 +3,7 @@
 import net.dv8tion.jda.api.JDA;
 import net.dv8tion.jda.api.JDABuilder;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.exceptions.InvalidTokenException;
 import net.dv8tion.jda.api.requests.GatewayIntent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -12,7 +13,6 @@
 import org.togetherjava.tjbot.config.Config;
 import org.togetherjava.tjbot.db.Database;
 
-import javax.security.auth.login.LoginException;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -79,7 +79,7 @@ public static void runBot(Config config) {
             Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath());
 
             JDA jda = JDABuilder.createDefault(config.getToken())
-                .enableIntents(GatewayIntent.GUILD_MEMBERS)
+                .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT)
                 .build();
 
             jda.awaitReady();
@@ -91,7 +91,7 @@ public static void runBot(Config config) {
             jda.addEventListener(core);
 
             logger.info("Bot is ready");
-        } catch (LoginException e) {
+        } catch (InvalidTokenException e) {
             logger.error("Failed to login", e);
         } catch (InterruptedException e) {
             logger.error("Interrupted while waiting for setup to complete", e);
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java
index f7cf697420..3a5e5af1f3 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/BotCommandAdapter.java
@@ -51,7 +51,7 @@ public abstract class BotCommandAdapter implements BotCommand {
      * @param visibility the visibility of the command
      */
     protected BotCommandAdapter(CommandData data, CommandVisibility visibility) {
-        this.data = Objects.requireNonNull(data, "The data shouldn't be null");
+        this.data = data.setGuildOnly(visibility == CommandVisibility.GUILD);
         this.visibility = Objects.requireNonNull(visibility, "The visibility shouldn't be null");
 
         this.name = data.getName();
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java
index 5a29c0c873..912f264450 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/MessageContextCommand.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands;
 
-import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
@@ -50,7 +49,7 @@ public interface MessageContextCommand extends BotCommand {
      * 
      * Buttons or menus have to be created with a component ID (see
      * {@link ComponentInteraction#getComponentId()},
-     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, Emoji)})
+     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)})
      * in a very specific format, otherwise the core system will fail to identify the command that
      * corresponded to the button or menu click event and is unable to route it back.
      * 
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java
index 53b5456d65..ba72609bc2 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/SlashCommand.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands;
 
-import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
@@ -81,7 +80,7 @@ public interface SlashCommand extends BotCommand {
      * 
      * Buttons or menus have to be created with a component ID (see
      * {@link ComponentInteraction#getComponentId()},
-     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, Emoji)})
+     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)})
      * in a very specific format, otherwise the core system will fail to identify the command that
      * corresponded to the button or menu click event and is unable to route it back.
      * 
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java
index 95e278bbad..cb1cab4127 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/UserContextCommand.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands;
 
-import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
@@ -51,7 +50,7 @@ public interface UserContextCommand extends BotCommand {
      * 
      * Buttons or menus have to be created with a component ID (see
      * {@link ComponentInteraction#getComponentId()},
-     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, Emoji)})
+     * {@link net.dv8tion.jda.api.interactions.components.buttons.Button#of(ButtonStyle, String, String)})
      * in a very specific format, otherwise the core system will fail to identify the command that
      * corresponded to the button or menu click event and is unable to route it back.
      * 
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java
index 096bb3327a..05a372eca1 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/RoleSelectCommand.java
@@ -3,6 +3,7 @@
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.Permission;
 import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.emoji.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
 import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
@@ -183,9 +184,8 @@ private void sendRoleSelectionMenu(final CommandInteraction event,
             .map(RoleSelectCommand::mapToSelectOption)
             .forEach(menu::addOptions);
 
-        OptionMapping titleOption = event.getOption(TITLE_OPTION);
-        String title = titleOption == null ? "Select your roles:" : titleOption.getAsString();
-
+        String title =
+                event.getOption(TITLE_OPTION, "Select your roles:", OptionMapping::getAsString);
         MessageEmbed embed = createEmbed(title, event.getOption(DESCRIPTION_OPTION).getAsString());
 
         event.replyEmbeds(embed).addActionRow(menu.build()).queue();
@@ -196,7 +196,7 @@ private static SelectOption mapToSelectOption(Role role) {
 
         SelectOption option = SelectOption.of(role.getName(), role.getId());
         if (null != roleIcon && roleIcon.isEmoji()) {
-            option = option.withEmoji((Emoji.fromUnicode(roleIcon.getEmoji())));
+            option = option.withEmoji(Emoji.fromUnicode(roleIcon.getEmoji()));
         }
 
         return option;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java
index f6bc7965b7..3ca76e4ae3 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/SuggestionsUpDownVoter.java
@@ -1,8 +1,9 @@
 package org.togetherjava.tjbot.commands.basic;
 
-import net.dv8tion.jda.api.entities.Emote;
 import net.dv8tion.jda.api.entities.Guild;
 import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.emoji.Emoji;
+import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.exceptions.ErrorResponseException;
 import net.dv8tion.jda.api.requests.ErrorResponse;
@@ -22,8 +23,8 @@
 public final class SuggestionsUpDownVoter extends MessageReceiverAdapter {
     private static final Logger logger = LoggerFactory.getLogger(SuggestionsUpDownVoter.class);
     private static final int TITLE_MAX_LENGTH = 60;
-    private static final String FALLBACK_UP_VOTE = "👍";
-    private static final String FALLBACK_DOWN_VOTE = "👎";
+    private static final Emoji FALLBACK_UP_VOTE = Emoji.fromUnicode("👍");
+    private static final Emoji FALLBACK_DOWN_VOTE = Emoji.fromUnicode("👎");
 
     private final SuggestionsConfig config;
 
@@ -68,13 +69,13 @@ private static void createThread(Message message) {
         message.createThreadChannel(title).queue();
     }
 
-    private static void reactWith(String emoteName, String fallbackUnicodeEmote, Guild guild,
+    private static void reactWith(String emojiName, Emoji fallbackEmoji, Guild guild,
             Message message) {
-        getEmoteByName(emoteName, guild).map(message::addReaction).orElseGet(() -> {
+        getEmojiByName(emojiName, guild).map(message::addReaction).orElseGet(() -> {
             logger.warn(
-                    "Unable to vote on a suggestion with the configured emote ('{}'), using fallback instead.",
-                    emoteName);
-            return message.addReaction(fallbackUnicodeEmote);
+                    "Unable to vote on a suggestion with the configured emoji ('{}'), using fallback instead.",
+                    emojiName);
+            return message.addReaction(fallbackEmoji);
         }).queue(ignored -> {
         }, exception -> {
             if (exception instanceof ErrorResponseException responseException
@@ -88,7 +89,7 @@ private static void reactWith(String emoteName, String fallbackUnicodeEmote, Gui
         });
     }
 
-    private static Optional getEmoteByName(String name, Guild guild) {
-        return guild.getEmotesByName(name, false).stream().findAny();
+    private static Optional getEmojiByName(String name, Guild guild) {
+        return guild.getEmojisByName(name, false).stream().findAny();
     }
 }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java
index 1322c62591..033616ab34 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/VcActivityCommand.java
@@ -4,7 +4,7 @@
 import net.dv8tion.jda.api.entities.GuildVoiceState;
 import net.dv8tion.jda.api.entities.Invite;
 import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.VoiceChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.interactions.commands.Command;
 import net.dv8tion.jda.api.interactions.commands.OptionMapping;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java
index dd8035e8d0..0a746965d8 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdGenerator.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands.componentids;
 
-import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
@@ -26,7 +25,7 @@ public interface ComponentIdGenerator {
      * interactions, such as button or selection menus.
      * 
      * See {@link ComponentInteraction#getComponentId()} and
-     * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the generated ID can be
+     * {@link Button#of(ButtonStyle, String, String)} for details on where the generated ID can be
      * used.
      *
      * @param componentId the component ID payload to persist and generate a valid ID for
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java
index 4e17409c6e..83173f0474 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/componentids/ComponentIdParser.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands.componentids;
 
-import net.dv8tion.jda.api.entities.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
@@ -28,7 +27,7 @@ public interface ComponentIdParser {
      * interactions, such as button or selection menus.
      * 
      * See {@link ComponentInteraction#getComponentId()} and
-     * {@link Button#of(ButtonStyle, String, Emoji)} for details on where the ID was originally
+     * {@link Button#of(ButtonStyle, String, String)} for details on where the ID was originally
      * transported with.
      *
      * @param uuid the UUID to parse which represents the component ID
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java
index c36b37730d..9e145b6b2e 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/filesharing/FileSharingMessageListener.java
@@ -2,10 +2,10 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import net.dv8tion.jda.api.entities.ChannelType;
 import net.dv8tion.jda.api.entities.Message;
-import net.dv8tion.jda.api.entities.ThreadChannel;
 import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
 import org.slf4j.Logger;
@@ -114,7 +114,8 @@ private void processAttachments(MessageReceivedEvent event,
 
         List> tasks = new ArrayList<>();
         for (Message.Attachment attachment : attachments) {
-            CompletableFuture task = attachment.retrieveInputStream()
+            CompletableFuture task = attachment.getProxy()
+                .download()
                 .thenApply(this::readAttachment)
                 .thenAccept(
                         content -> nameToFile.put(getNameOf(attachment), new GistFile(content)));
@@ -217,7 +218,7 @@ private boolean isHelpThread(MessageReceivedEvent event) {
             return false;
         }
 
-        ThreadChannel thread = event.getThreadChannel();
+        ThreadChannel thread = event.getChannel().asThreadChannel();
         String rootChannelName = thread.getParentChannel().getName();
         return isStagingChannelName.test(rootChannelName)
                 || isOverviewChannelName.test(rootChannelName);
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java
index 91ad35f2c0..59ce0f3fff 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AskCommand.java
@@ -1,6 +1,11 @@
 package org.togetherjava.tjbot.commands.help;
 
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.IMentionable;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.exceptions.ErrorResponseException;
 import net.dv8tion.jda.api.interactions.InteractionHook;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java
index 2623c1b13f..5d20b179e1 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/AutoPruneHelperRoutine.java
@@ -4,7 +4,7 @@
 import net.dv8tion.jda.api.entities.Guild;
 import net.dv8tion.jda.api.entities.Member;
 import net.dv8tion.jda.api.entities.Role;
-import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.Routine;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java
index 0ebe47f21e..768341fad0 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/BotMessageCleanup.java
@@ -2,9 +2,9 @@
 
 import net.dv8tion.jda.api.JDA;
 import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.GuildMessageChannel;
 import net.dv8tion.jda.api.entities.Message;
-import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
 import net.dv8tion.jda.api.requests.RestAction;
 import net.dv8tion.jda.internal.requests.CompletedRestAction;
 import org.slf4j.Logger;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
index 59f4a54a25..2831d23400 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpSystemHelper.java
@@ -2,10 +2,17 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.Channel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.requests.RestAction;
-import net.dv8tion.jda.api.requests.restaction.MessageAction;
+import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
+import net.dv8tion.jda.api.utils.FileUpload;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import net.dv8tion.jda.internal.requests.CompletedRestAction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -110,9 +117,10 @@ RestAction sendExplanationMessage(MessageChannel threadChannel) {
                 HelpSystemHelper.embedWith(
                         "Don't forget to close your thread using the command **/help-thread close** when your question has been answered, thanks."));
 
-        MessageAction action = threadChannel.sendMessage(message);
+        MessageCreateAction action = threadChannel.sendMessage(message);
         if (useCodeSyntaxExampleImage) {
-            action = action.addFile(codeSyntaxExampleData, CODE_SYNTAX_EXAMPLE_PATH);
+            action = action
+                .addFiles(FileUpload.fromData(codeSyntaxExampleData, CODE_SYNTAX_EXAMPLE_PATH));
         }
         return action.setEmbeds(embeds);
     }
@@ -295,7 +303,9 @@ private void executeUncategorizedAdviceCheck(long threadChannelId, long authorId
                             Hey there 👋 You have to select a category for your help thread, otherwise nobody can see your question.
                             Please use the `/help-thread change category` slash-command and pick what fits best, thanks 🙂
                             """);
-            Message message = new MessageBuilder(author.getAsMention()).setEmbeds(embed).build();
+            MessageCreateData message = new MessageCreateBuilder().setContent(author.getAsMention())
+                .setEmbeds(embed)
+                .build();
 
             return threadChannel.sendMessage(message);
         }).queue();
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java
index 79553fcdee..75db5e8f1a 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadActivityUpdater.java
@@ -1,7 +1,12 @@
 package org.togetherjava.tjbot.commands.help;
 
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.requests.RestAction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java
index 312d170b1e..ef98c5f523 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadAutoArchiver.java
@@ -2,7 +2,11 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.utils.TimeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java
index 04c9b78cb5..b37c503537 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadCommand.java
@@ -3,7 +3,11 @@
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import net.dv8tion.jda.api.EmbedBuilder;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.Role;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.interactions.InteractionHook;
 import net.dv8tion.jda.api.interactions.commands.OptionType;
@@ -11,7 +15,7 @@
 import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
 import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
 import net.dv8tion.jda.api.requests.RestAction;
-import net.dv8tion.jda.api.requests.restaction.WebhookMessageUpdateAction;
+import net.dv8tion.jda.api.requests.restaction.WebhookMessageEditAction;
 import org.togetherjava.tjbot.commands.CommandVisibility;
 import org.togetherjava.tjbot.commands.SlashCommandAdapter;
 import org.togetherjava.tjbot.config.Config;
@@ -92,7 +96,7 @@ public HelpThreadCommand(Config config, HelpSystemHelper helper) {
 
     @Override
     public void onSlashCommand(SlashCommandInteractionEvent event) {
-        ThreadChannel helpThread = event.getThreadChannel();
+        ThreadChannel helpThread = event.getChannel().asThreadChannel();
 
         Subcommand invokedSubcommand =
                 Objects.requireNonNull(nameToSubcommand.get(event.getSubcommandName()));
@@ -153,7 +157,7 @@ private void refreshCooldownFor(Subcommand subcommand, ThreadChannel helpThread)
     private RestAction sendCategoryChangedMessage(Guild guild, InteractionHook hook,
             ThreadChannel helpThread, String category) {
         String changedContent = "Changed the category to **%s**.".formatted(category);
-        WebhookMessageUpdateAction action = hook.editOriginal(changedContent);
+        WebhookMessageEditAction action = hook.editOriginal(changedContent);
 
         Optional helperRole = helper.handleFindRoleForCategory(category, guild);
         if (helperRole.isEmpty()) {
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java
index eb32a0e5ac..3337e18f6e 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/HelpThreadOverviewUpdater.java
@@ -1,10 +1,17 @@
 package org.togetherjava.tjbot.commands.help;
 
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.MessageBuilder;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageType;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageEditData;
 import net.dv8tion.jda.internal.requests.CompletedRestAction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -112,7 +119,7 @@ private void updateOverview(TextChannel overviewChannel) {
         List activeThreads = helper.getActiveThreadsIn(overviewChannel);
         logger.debug("Found {} active questions", activeThreads.size());
 
-        Message message = new MessageBuilder()
+        MessageEditData message = new MessageEditBuilder()
             .setContent(STATUS_TITLE + "\n\n" + createDescription(activeThreads))
             .build();
 
@@ -165,7 +172,7 @@ private static boolean isStatusMessage(Message message) {
     }
 
     private RestAction sendUpdatedOverview(@Nullable Message statusMessage,
-            Message updatedStatusMessage, MessageChannel overviewChannel) {
+            MessageEditData updatedStatusMessage, MessageChannel overviewChannel) {
         logger.debug("Sending the updated question overview");
         if (statusMessage == null) {
             int currentFailures = FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.incrementAndGet();
@@ -174,7 +181,8 @@ private RestAction sendUpdatedOverview(@Nullable Message statusMessage,
                         "Failed to locate the question overview too often ({} times), sending a fresh message instead.",
                         currentFailures);
                 FIND_STATUS_MESSAGE_CONSECUTIVE_FAILURES.set(0);
-                return overviewChannel.sendMessage(updatedStatusMessage);
+                return overviewChannel
+                    .sendMessage(MessageCreateData.fromEditData(updatedStatusMessage));
             }
 
             logger.info(
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
index 629d022128..38289df454 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/ImplicitAskListener.java
@@ -3,13 +3,16 @@
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
 import net.dv8tion.jda.api.EmbedBuilder;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.exceptions.ErrorResponseException;
 import net.dv8tion.jda.api.requests.ErrorResponse;
 import net.dv8tion.jda.api.requests.RestAction;
-import net.dv8tion.jda.api.requests.restaction.MessageAction;
+import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
@@ -163,14 +166,11 @@ private RestAction> handleEvent(ThreadChannel threadChannel, Message message,
             .flatMap(any -> notifyUser(threadChannel, message))
             .flatMap(any -> message.delete())
             .flatMap(any -> helper.sendExplanationMessage(threadChannel))
-            .map(any -> {
-                helper.scheduleUncategorizedAdviceCheck(threadChannel.getIdLong(),
-                        author.getIdLong());
-                return null;
-            });
+            .onSuccess(any -> helper.scheduleUncategorizedAdviceCheck(threadChannel.getIdLong(),
+                    author.getIdLong()));
     }
 
-    private static MessageAction sendInitialMessage(ThreadChannel threadChannel,
+    private static MessageCreateAction sendInitialMessage(ThreadChannel threadChannel,
             Message originalMessage, String title) {
         String content = originalMessage.getContentRaw();
         Member author = originalMessage.getMember();
@@ -181,17 +181,20 @@ private static MessageAction sendInitialMessage(ThreadChannel threadChannel,
             .setColor(HelpSystemHelper.AMBIENT_COLOR)
             .build();
 
-        Message threadMessage = new MessageBuilder(
-                """
-                        %s has a question about '**%s**' and will send the details now.
+        MessageCreateData threadMessage = new MessageCreateBuilder()
+            .setContent(
+                    """
+                            %s has a question about '**%s**' and will send the details now.
 
-                        Please use `/help-thread change category` to greatly increase the visibility of the question."""
-                    .formatted(author, title)).setEmbeds(embed).build();
+                            Please use `/help-thread change category` to greatly increase the visibility of the question."""
+                        .formatted(author, title))
+            .setEmbeds(embed)
+            .build();
 
         return threadChannel.sendMessage(threadMessage);
     }
 
-    private static MessageAction notifyUser(IMentionable threadChannel, Message message) {
+    private static MessageCreateAction notifyUser(IMentionable threadChannel, Message message) {
         return message.getChannel()
             .sendMessage(
                     """
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java
index 3da99a69e7..0d68901753 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/help/OnGuildLeaveCloseThreadListener.java
@@ -2,7 +2,7 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
 import net.dv8tion.jda.api.hooks.ListenerAdapter;
 import org.slf4j.Logger;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java
index 3e6d707e9c..ead51436f8 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommand.java
@@ -6,6 +6,7 @@
 import net.dv8tion.jda.api.interactions.commands.OptionType;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
 import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
+import net.dv8tion.jda.api.utils.FileUpload;
 import org.scilab.forge.jlatexmath.ParseException;
 import org.scilab.forge.jlatexmath.TeXConstants;
 import org.scilab.forge.jlatexmath.TeXFormula;
@@ -110,7 +111,8 @@ private void sendImage(IDeferrableCallback event, String userID, Image image)
             throws IOException {
         ByteArrayOutputStream renderedTextImageStream = getRenderedTextImageStream(image);
         event.getHook()
-            .editOriginal(renderedTextImageStream.toByteArray(), "tex.png")
+            .editOriginalAttachments(
+                    FileUpload.fromData(renderedTextImageStream.toByteArray(), "tex.png"))
             .setActionRow(Button.of(ButtonStyle.DANGER, generateComponentId(userID), "Delete"))
             .queue();
     }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java
index c599368bb0..fb549df9ce 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaCommand.java
@@ -1,18 +1,18 @@
 package org.togetherjava.tjbot.commands.mathcommands.wolframalpha;
 
 import io.mikael.urlbuilder.UrlBuilder;
-import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.interactions.callbacks.IDeferrableCallback;
 import net.dv8tion.jda.api.interactions.commands.OptionType;
-import net.dv8tion.jda.api.requests.restaction.WebhookMessageUpdateAction;
 import org.togetherjava.tjbot.commands.CommandVisibility;
+import net.dv8tion.jda.api.utils.FileUpload;
 import org.togetherjava.tjbot.commands.SlashCommandAdapter;
 import org.togetherjava.tjbot.config.Config;
 
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
 /**
@@ -74,13 +74,11 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
 
     private static void sendResponse(WolframAlphaHandler.HandlerResponse response,
             IDeferrableCallback event) {
-        WebhookMessageUpdateAction action =
-                event.getHook().editOriginalEmbeds(response.embeds());
+        List files = response.attachments()
+            .stream()
+            .map(attachment -> FileUpload.fromData(attachment.data(), attachment.name()))
+            .toList();
 
-        for (WolframAlphaHandler.Attachment attachment : response.attachments()) {
-            action = action.addFile(attachment.data(), attachment.name());
-        }
-
-        action.queue();
+        event.getHook().editOriginalEmbeds(response.embeds()).setFiles(files).queue();
     }
 }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java
index 2d53992e68..749ddee1fe 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/mathcommands/wolframalpha/WolframAlphaHandler.java
@@ -3,6 +3,7 @@
 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 import io.mikael.urlbuilder.UrlBuilder;
 import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,18 +37,12 @@ final class WolframAlphaHandler {
      * Discord and do not provide a nice user experience anymore.
      */
     private static final int MAX_IMAGE_HEIGHT_PX = 400;
-    /**
-     * Maximum amount of embeds Discord supports.
-     * 
-     * This should be replaced with a constant provided by JDA, once it does offer one.
-     */
-    private static final int MAX_EMBEDS = 10;
     /**
      * Maximum amount of tiles to send.
      * 
      * One embed is used as initial description and summary.
      */
-    private static final int MAX_TILES = MAX_EMBEDS - 1;
+    private static final int MAX_TILES = Message.MAX_EMBED_COUNT - 1;
 
     private final String query;
     private final String userApiQuery;
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java
index b2bce6613f..af29d1013f 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListener.java
@@ -1,12 +1,13 @@
 package org.togetherjava.tjbot.commands.mediaonly;
 
 import net.dv8tion.jda.api.EmbedBuilder;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import net.dv8tion.jda.api.entities.MessageType;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
 import org.togetherjava.tjbot.config.Config;
 
@@ -59,10 +60,10 @@ private RestAction dmUser(Message message) {
                     .setColor(Color.ORANGE)
                     .build();
 
-        Message dmMessage = new MessageBuilder(
+        MessageCreateData dmMessage = new MessageCreateBuilder().setContent(
                 "Hey there, you posted a message without media (image, video, link) in a media-only channel. Please see the description of the channel for details and then repost with media attached, thanks 😀")
-                    .setEmbeds(originalMessageEmbed)
-                    .build();
+            .setEmbeds(originalMessageEmbed)
+            .build();
 
         return message.getAuthor()
             .openPrivateChannel()
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java
index 8fceb3adb6..f8e9f560bc 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/AuditCommand.java
@@ -2,17 +2,21 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.MessageBuilder;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.User;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
 import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
 import net.dv8tion.jda.api.interactions.commands.OptionMapping;
 import net.dv8tion.jda.api.interactions.commands.OptionType;
-import net.dv8tion.jda.api.interactions.components.ActionRow;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
 import net.dv8tion.jda.api.requests.RestAction;
 import net.dv8tion.jda.api.utils.TimeUtil;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageRequest;
 import net.dv8tion.jda.internal.requests.CompletedRestAction;
 import org.togetherjava.tjbot.commands.CommandVisibility;
 import org.togetherjava.tjbot.commands.SlashCommandAdapter;
@@ -21,6 +25,7 @@
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.util.*;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 /**
@@ -68,8 +73,10 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
             return;
         }
 
-        auditUser(guild.getIdLong(), target.getIdLong(), event.getMember().getIdLong(), -1,
-                event.getJDA()).flatMap(event::reply).queue();
+        auditUser(MessageCreateBuilder::new, guild.getIdLong(), target.getIdLong(),
+                event.getMember().getIdLong(), -1, event.getJDA()).map(MessageCreateBuilder::build)
+                    .flatMap(event::reply)
+                    .queue();
     }
 
     private boolean handleChecks(Member bot, Member author, @Nullable Member target,
@@ -86,7 +93,8 @@ private boolean handleChecks(Member bot, Member author, @Nullable Member target,
      *        can contain {@link AuditCommand#MAX_PAGE_LENGTH} actions, {@code -1} encodes the last
      *        page
      */
-    private RestAction auditUser(long guildId, long targetId, long callerId,
+    private > RestAction auditUser(
+            Supplier messageBuilderSupplier, long guildId, long targetId, long callerId,
             int pageNumber, JDA jda) {
         List actions = actionsStore.getActionsByTargetAscending(guildId, targetId);
         List> groupedActions = groupActionsByPages(actions);
@@ -103,8 +111,8 @@ private RestAction auditUser(long guildId, long targetId, long callerId
             .map(user -> createSummaryEmbed(user, actions))
             .flatMap(auditEmbed -> attachEmbedFields(auditEmbed, groupedActions, pageNumberInLimits,
                     totalPages, jda))
-            .map(auditEmbed -> attachPageTurnButtons(auditEmbed, pageNumberInLimits, totalPages,
-                    guildId, targetId, callerId));
+            .map(auditEmbed -> attachPageTurnButtons(messageBuilderSupplier, auditEmbed,
+                    pageNumberInLimits, totalPages, guildId, targetId, callerId));
     }
 
     private List> groupActionsByPages(List actions) {
@@ -197,20 +205,22 @@ private static String formatTime(Instant when) {
         return TimeUtil.getDateTimeString(when.atOffset(ZoneOffset.UTC));
     }
 
-    private Message attachPageTurnButtons(EmbedBuilder auditEmbed, int pageNumber, int totalPages,
-            long guildId, long targetId, long callerId) {
-        var messageBuilder = new MessageBuilder(auditEmbed.build());
+    private > R attachPageTurnButtons(
+            Supplier messageBuilderSupplier, EmbedBuilder auditEmbed, int pageNumber,
+            int totalPages, long guildId, long targetId, long callerId) {
+        var messageBuilder = messageBuilderSupplier.get();
+        messageBuilder.setEmbeds(auditEmbed.build());
 
         if (totalPages <= 1) {
-            return messageBuilder.build();
+            return messageBuilder;
         }
-        ActionRow pageTurnButtons =
+        List pageTurnButtons =
                 createPageTurnButtons(guildId, targetId, callerId, pageNumber, totalPages);
 
-        return messageBuilder.setActionRows(pageTurnButtons).build();
+        return messageBuilder.setActionRow(pageTurnButtons);
     }
 
-    private ActionRow createPageTurnButtons(long guildId, long targetId, long callerId,
+    private List createPageTurnButtons(long guildId, long targetId, long callerId,
             int pageNumber, int totalPages) {
         int previousButtonTurnPageBy = -1;
         Button previousButton = createPageTurnButton(PREVIOUS_BUTTON_LABEL, guildId, targetId,
@@ -226,7 +236,7 @@ private ActionRow createPageTurnButtons(long guildId, long targetId, long caller
             nextButton = nextButton.asDisabled();
         }
 
-        return ActionRow.of(previousButton, nextButton);
+        return List.of(previousButton, nextButton);
     }
 
     private Button createPageTurnButton(String label, long guildId, long targetId, long callerId,
@@ -256,8 +266,7 @@ public void onButtonClick(ButtonInteractionEvent event, List args) {
         long targetId = Long.parseLong(args.get(1));
         int pageToDisplay = currentPage + turnPageBy;
 
-        auditUser(guildId, targetId, buttonUserId, pageToDisplay, event.getJDA())
-            .flatMap(event::editMessage)
-            .queue();
+        auditUser(MessageEditBuilder::new, guildId, targetId, buttonUserId, pageToDisplay,
+                event.getJDA()).map(MessageEditBuilder::build).flatMap(event::editMessage).queue();
     }
 }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java
index d42838dc35..a92d387c2e 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java
@@ -24,6 +24,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 
 /**
  * This command can ban users and optionally remove their messages from the past days. Banning can
@@ -157,7 +158,7 @@ private AuditableRestAction banUser(User target, Member author,
         actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(),
                 ModerationAction.BAN, expiresAt, reason);
 
-        return guild.ban(target, deleteHistoryDays, reason);
+        return guild.ban(target, deleteHistoryDays, TimeUnit.DAYS).reason(reason);
     }
 
     @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"})
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java
index 04fa4d3184..b237cecd07 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java
@@ -89,7 +89,7 @@ private AuditableRestAction kickUser(Member target, Member author, String
         actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(),
                 ModerationAction.KICK, null, reason);
 
-        return guild.kick(target, reason).reason(reason);
+        return guild.kick(target).reason(reason);
     }
 
     private static MessageEmbed sendFeedback(boolean hasSentDm, Member target, Member author,
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java
index e2e56d684e..130cdbf2eb 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java
@@ -1,11 +1,12 @@
 package org.togetherjava.tjbot.commands.moderation.attachment;
 
 import net.dv8tion.jda.api.EmbedBuilder;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
 import org.togetherjava.tjbot.config.Config;
 import org.togetherjava.tjbot.moderation.ModAuditLogWriter;
@@ -49,13 +50,12 @@ private void handleBadMessage(Message message) {
     }
 
     private RestAction dmUser(Message message) {
-        Message dmMessage = createDmMessage(message);
         return message.getAuthor()
             .openPrivateChannel()
-            .flatMap(privateChannel -> privateChannel.sendMessage(dmMessage));
+            .flatMap(privateChannel -> privateChannel.sendMessage(createDmMessage(message)));
     }
 
-    private Message createDmMessage(Message originalMessage) {
+    private MessageCreateData createDmMessage(Message originalMessage) {
         String contentRaw = originalMessage.getContentRaw();
         String blacklistedAttachments =
                 String.join(", ", getBlacklistedAttachmentsFromMessage(originalMessage));
@@ -71,17 +71,20 @@ private Message createDmMessage(Message originalMessage) {
 
         // No embed needed if there was no message from the user
         if (contentRaw.isEmpty()) {
-            return new MessageBuilder(dmMessageContent).build();
+            return new MessageCreateBuilder().setContent(dmMessageContent).build();
         }
         return createBaseResponse(contentRaw, dmMessageContent);
     }
 
-    private Message createBaseResponse(String originalMessageContent, String dmMessageContent) {
+    private MessageCreateData createBaseResponse(String originalMessageContent,
+            String dmMessageContent) {
         MessageEmbed originalMessageEmbed =
                 new EmbedBuilder().setDescription(originalMessageContent)
                     .setColor(Color.ORANGE)
                     .build();
-        return new MessageBuilder(dmMessageContent).setEmbeds(originalMessageEmbed).build();
+        return new MessageCreateBuilder().setContent(dmMessageContent)
+            .setEmbeds(originalMessageEmbed)
+            .build();
     }
 
     private List getBlacklistedAttachmentsFromMessage(Message originalMessage) {
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java
index e488c34387..5cf745fdde 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/scam/ScamBlocker.java
@@ -2,16 +2,17 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import net.dv8tion.jda.api.exceptions.ErrorHandler;
-import net.dv8tion.jda.api.interactions.components.ActionRow;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
 import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
 import net.dv8tion.jda.api.requests.ErrorResponse;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
@@ -26,7 +27,6 @@
 import org.togetherjava.tjbot.config.Config;
 import org.togetherjava.tjbot.config.ScamBlockerConfig;
 
-import javax.annotation.Nullable;
 import java.awt.Color;
 import java.util.*;
 import java.util.function.Consumer;
@@ -168,7 +168,7 @@ private void takeActionAutoDeleteAndQuarantine(MessageReceivedEvent event) {
         deleteMessage(event);
         quarantineAuthor(event);
         dmUser(event);
-        reportScamMessage(event, "Detected and handled scam", null);
+        reportScamMessage(event, "Detected and handled scam", List.of());
     }
 
     private void addScamToHistory(MessageReceivedEvent event) {
@@ -203,7 +203,7 @@ private void quarantineAuthor(Guild guild, Member author, SelfUser bot) {
     }
 
     private void reportScamMessage(MessageReceivedEvent event, String reportTitle,
-            @Nullable ActionRow confirmDialog) {
+            List extends Button> confirmDialog) {
         Guild guild = event.getGuild();
         Optional reportChannel = getReportChannel(guild);
         if (reportChannel.isEmpty()) {
@@ -222,8 +222,8 @@ private void reportScamMessage(MessageReceivedEvent event, String reportTitle,
                     .setColor(AMBIENT_COLOR)
                     .setFooter(author.getId())
                     .build();
-        Message message =
-                new MessageBuilder().setEmbeds(embed).setActionRows(confirmDialog).build();
+        MessageCreateData message =
+                new MessageCreateBuilder().setEmbeds(embed).setActionRow(confirmDialog).build();
 
         reportChannel.orElseThrow().sendMessage(message).queue();
     }
@@ -252,13 +252,13 @@ private Optional getReportChannel(Guild guild) {
         return guild.getTextChannelCache().stream().filter(isReportChannel).findAny();
     }
 
-    private ActionRow createConfirmDialog(MessageReceivedEvent event) {
+    private List createConfirmDialog(MessageReceivedEvent event) {
         ComponentIdArguments args = new ComponentIdArguments(mode, event.getGuild().getIdLong(),
                 event.getChannel().getIdLong(), event.getMessageIdLong(),
                 event.getAuthor().getIdLong(),
                 ScamHistoryStore.hashMessageContent(event.getMessage()));
 
-        return ActionRow.of(Button.success(generateComponentId(args), "Yes"),
+        return List.of(Button.success(generateComponentId(args), "Yes"),
                 Button.danger(generateComponentId(args), "No"));
     }
 
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java
index aa1a40e096..6b140773c6 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryMuteAction.java
@@ -39,8 +39,7 @@ public ModerationAction getRevokeType() {
     @Override
     public RestAction revokeAction(Guild guild, User target, String reason) {
         return guild
-            .removeRoleFromMember(target.getIdLong(),
-                    ModerationUtils.getMutedRole(guild, config).orElseThrow())
+            .removeRoleFromMember(target, ModerationUtils.getMutedRole(guild, config).orElseThrow())
             .reason(reason);
     }
 }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java
index ca44ddc746..4d372f259c 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/temp/TemporaryQuarantineAction.java
@@ -39,7 +39,7 @@ public ModerationAction getRevokeType() {
     @Override
     public RestAction revokeAction(Guild guild, User target, String reason) {
         return guild
-            .removeRoleFromMember(target.getIdLong(),
+            .removeRoleFromMember(target,
                     ModerationUtils.getQuarantinedRole(guild, config).orElseThrow())
             .reason(reason);
     }
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java
index 621cbdce73..d71f5e5d5a 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java
@@ -2,12 +2,12 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.MessageChannel;
 import net.dv8tion.jda.api.entities.MessageEmbed;
-import net.dv8tion.jda.api.entities.PrivateChannel;
 import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.requests.RestAction;
-import net.dv8tion.jda.api.requests.restaction.MessageAction;
+import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.Routine;
@@ -95,9 +95,9 @@ private static RestAction createDmReminderRoute(JDA jda, long aut
 
     private static void sendReminderViaRoute(RestAction routeAction, long id,
             CharSequence content, TemporalAccessor createdAt) {
-        Function sendMessage = route -> route.channel
+        Function sendMessage = route -> route.channel
             .sendMessageEmbeds(createReminderEmbed(content, createdAt, route.target()))
-            .content(route.description());
+            .setContent(route.description());
 
         Consumer logFailure = failure -> logger.warn(
                 """
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java
index cc481b3ce0..771d0f9216 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/system/BotCore.java
@@ -1,7 +1,7 @@
 package org.togetherjava.tjbot.commands.system;
 
 import net.dv8tion.jda.api.JDA;
-import net.dv8tion.jda.api.entities.Channel;
+import net.dv8tion.jda.api.entities.channel.Channel;
 import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
@@ -12,7 +12,6 @@
 import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
 import net.dv8tion.jda.api.hooks.ListenerAdapter;
 import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Unmodifiable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -270,7 +269,7 @@ public void onSelectMenuInteraction(SelectMenuInteractionEvent event) {
     }
 
     @Override
-    public void onMessageContextInteraction(@NotNull final MessageContextInteractionEvent event) {
+    public void onMessageContextInteraction(final MessageContextInteractionEvent event) {
         String name = event.getName();
 
         logger.debug("Received message context command '{}' (#{}) on guild '{}'", name,
@@ -281,7 +280,7 @@ public void onMessageContextInteraction(@NotNull final MessageContextInteraction
     }
 
     @Override
-    public void onUserContextInteraction(@NotNull final UserContextInteractionEvent event) {
+    public void onUserContextInteraction(final UserContextInteractionEvent event) {
         String name = event.getName();
 
         logger.debug("Received user context command '{}' (#{}) on guild '{}'", name, event.getId(),
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
index f496ed104a..bcb3939834 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagManageCommand.java
@@ -10,6 +10,7 @@
 import net.dv8tion.jda.api.interactions.commands.OptionType;
 import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
 import net.dv8tion.jda.api.requests.ErrorResponse;
+import net.dv8tion.jda.api.utils.FileUpload;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.commands.CommandVisibility;
@@ -157,7 +158,8 @@ private void rawTag(SlashCommandInteractionEvent event) {
 
         String content = tagSystem.getTag(id).orElseThrow();
         event.reply("")
-            .addFile(content.getBytes(StandardCharsets.UTF_8), CONTENT_FILE_NAME)
+            .addFiles(FileUpload.fromData(content.getBytes(StandardCharsets.UTF_8),
+                    CONTENT_FILE_NAME))
             .queue();
     }
 
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
index 8a34131be9..5d7d0cce41 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tags/TagSystem.java
@@ -1,6 +1,6 @@
 package org.togetherjava.tjbot.commands.tags;
 
-import net.dv8tion.jda.api.entities.Emoji;
+import net.dv8tion.jda.api.entities.emoji.Emoji;
 import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
 import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
@@ -44,8 +44,7 @@ public TagSystem(Database database) {
      */
     @SuppressWarnings("StaticMethodOnlyUsedInOneClass")
     static Button createDeleteButton(String componentId) {
-        return Button.of(ButtonStyle.DANGER, componentId, "Delete",
-                Emoji.fromUnicode("\uD83D\uDDD1")); // trash bin
+        return Button.of(ButtonStyle.DANGER, componentId, "Delete", Emoji.fromUnicode("🗑"));
     }
 
     /**
diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
index a363c61898..488b17a64f 100644
--- a/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
+++ b/application/src/main/java/org/togetherjava/tjbot/commands/tophelper/TopHelpersMessageListener.java
@@ -1,7 +1,7 @@
 package org.togetherjava.tjbot.commands.tophelper;
 
-import net.dv8tion.jda.api.entities.ChannelType;
-import net.dv8tion.jda.api.entities.ThreadChannel;
+import net.dv8tion.jda.api.entities.channel.ChannelType;
+import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
 import org.togetherjava.tjbot.commands.MessageReceiverAdapter;
 import org.togetherjava.tjbot.config.Config;
@@ -57,7 +57,7 @@ private boolean isHelpThread(MessageReceivedEvent event) {
             return false;
         }
 
-        ThreadChannel thread = event.getThreadChannel();
+        ThreadChannel thread = event.getChannel().asThreadChannel();
         String rootChannelName = thread.getParentChannel().getName();
         return isStagingChannelName.test(rootChannelName)
                 || isOverviewChannelName.test(rootChannelName);
diff --git a/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java
index d300031d23..0d080527c3 100644
--- a/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java
+++ b/application/src/main/java/org/togetherjava/tjbot/moderation/ModAuditLogWriter.java
@@ -2,17 +2,16 @@
 
 import net.dv8tion.jda.api.EmbedBuilder;
 import net.dv8tion.jda.api.entities.Guild;
-import net.dv8tion.jda.api.entities.TextChannel;
 import net.dv8tion.jda.api.entities.User;
-import net.dv8tion.jda.api.requests.restaction.MessageAction;
-import net.dv8tion.jda.api.utils.AttachmentOption;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
+import net.dv8tion.jda.api.utils.FileUpload;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.togetherjava.tjbot.config.Config;
 
 import javax.annotation.Nullable;
 import java.awt.Color;
-import java.io.File;
 import java.nio.charset.StandardCharsets;
 import java.time.temporal.TemporalAccessor;
 import java.util.Optional;
@@ -71,11 +70,12 @@ public void write(String title, String description, @Nullable User author,
             embedBuilder.setAuthor(author.getAsTag(), null, author.getAvatarUrl());
         }
 
-        MessageAction message =
+        MessageCreateAction message =
                 auditLogChannel.orElseThrow().sendMessageEmbeds(embedBuilder.build());
 
         for (Attachment attachment : attachments) {
-            message = message.addFile(attachment.getContentRaw(), attachment.name());
+            message = message
+                .addFiles(FileUpload.fromData(attachment.getContentRaw(), attachment.name()));
         }
         message.queue();
     }
@@ -103,7 +103,7 @@ public Optional getAndHandleModAuditLogChannel(Guild guild) {
 
     /**
      * Represents attachment to messages, as for example used by
-     * {@link MessageAction#addFile(File, String, AttachmentOption...)}.
+     * {@link MessageCreateAction#addFiles(FileUpload...)}.
      *
      * @param name the name of the attachment, example: {@code "foo.md"}
      * @param content the content of the attachment
diff --git a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java
index 7f4b6c5de0..2170f27a8b 100644
--- a/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java
+++ b/application/src/main/java/org/togetherjava/tjbot/routines/ModAuditLogRoutine.java
@@ -6,7 +6,12 @@
 import net.dv8tion.jda.api.audit.AuditLogChange;
 import net.dv8tion.jda.api.audit.AuditLogEntry;
 import net.dv8tion.jda.api.audit.AuditLogKey;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.ISnowflake;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.requests.RestAction;
 import net.dv8tion.jda.api.requests.restaction.pagination.AuditLogPaginationAction;
 import net.dv8tion.jda.api.requests.restaction.pagination.PaginationAction;
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommandTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommandTest.java
index 1f518eb13b..33fefdf308 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommandTest.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/mathcommands/TeXCommandTest.java
@@ -1,10 +1,12 @@
 package org.togetherjava.tjbot.commands.mathcommands;
 
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.utils.FileUpload;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentMatcher;
 import org.togetherjava.tjbot.commands.SlashCommand;
 import org.togetherjava.tjbot.jda.JdaTester;
 
@@ -35,8 +37,11 @@ private SlashCommandInteractionEvent triggerSlashCommand(String latex) {
     }
 
     private void verifySuccessfulResponse(SlashCommandInteractionEvent event, String query) {
+        ArgumentMatcher attachmentIsTexPng =
+                attachment -> attachment != null && "tex.png".equals(attachment.getName());
+
         verify(jdaTester.getInteractionHookMock(), description("Testing query: " + query))
-            .editOriginal(any(byte[].class), eq("tex.png"));
+            .editOriginalAttachments(argThat(attachmentIsTexPng));
     }
 
     private static List provideSupportedQueries() {
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListenerTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListenerTest.java
index 653d776970..f7f7123c34 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListenerTest.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/mediaonly/MediaOnlyChannelListenerTest.java
@@ -1,10 +1,11 @@
 package org.togetherjava.tjbot.commands.mediaonly;
 
 import net.dv8tion.jda.api.EmbedBuilder;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.togetherjava.tjbot.config.Config;
@@ -32,7 +33,7 @@ void setUp() {
     @Test
     void deletesMessageWithoutMedia() {
         // GIVEN a message without media
-        Message message = new MessageBuilder().setContent("any").build();
+        MessageCreateData message = new MessageCreateBuilder().setContent("any").build();
 
         // WHEN sending the message
         MessageReceivedEvent event = sendMessage(message);
@@ -45,7 +46,8 @@ void deletesMessageWithoutMedia() {
     void keepsMessageWithEmbed() {
         // GIVEN a message with an embed
         MessageEmbed embed = new EmbedBuilder().setDescription("any").build();
-        Message message = new MessageBuilder().setContent("any").setEmbeds(embed).build();
+        MessageCreateData message =
+                new MessageCreateBuilder().setContent("any").setEmbeds(embed).build();
 
         // WHEN sending the message
         MessageReceivedEvent event = sendMessage(message);
@@ -57,7 +59,7 @@ void keepsMessageWithEmbed() {
     @Test
     void keepsMessageWithAttachment() {
         // GIVEN a message with an attachment
-        Message message = new MessageBuilder().setContent("any").build();
+        MessageCreateData message = new MessageCreateBuilder().setContent("any").build();
         List attachments = List.of(mock(Message.Attachment.class));
 
         // WHEN sending the message
@@ -70,7 +72,7 @@ void keepsMessageWithAttachment() {
     @Test
     void keepsMessageWithLinkedMedia() {
         // GIVEN a message with media linked in the message
-        Message message = new MessageBuilder()
+        MessageCreateData message = new MessageCreateBuilder()
             .setContent("Check out this cute cat https://i.imgur.com/HLFByUJ.png")
             .build();
 
@@ -84,20 +86,20 @@ void keepsMessageWithLinkedMedia() {
     @Test
     void sendsAuthorDmUponDeletion() {
         // GIVEN a message without media
-        Message message = new MessageBuilder().setContent("any").build();
+        MessageCreateData message = new MessageCreateBuilder().setContent("any").build();
 
         // WHEN sending the message
         MessageReceivedEvent event = sendMessage(message);
 
         // THEN the author receives a DM
-        verify(jdaTester.getPrivateChannelSpy()).sendMessage(any(Message.class));
+        verify(jdaTester.getPrivateChannelSpy()).sendMessage(any(MessageCreateData.class));
     }
 
-    private MessageReceivedEvent sendMessage(Message message) {
+    private MessageReceivedEvent sendMessage(MessageCreateData message) {
         return sendMessage(message, List.of());
     }
 
-    private MessageReceivedEvent sendMessage(Message message,
+    private MessageReceivedEvent sendMessage(MessageCreateData message,
             List attachments) {
         MessageReceivedEvent event = jdaTester.createMessageReceiveEvent(message, attachments);
         mediaOnlyChannelListener.onMessageReceived(event);
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RawReminderTestHelper.java b/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RawReminderTestHelper.java
index 2f676d7d16..3d7968b35b 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RawReminderTestHelper.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RawReminderTestHelper.java
@@ -1,7 +1,7 @@
 package org.togetherjava.tjbot.commands.reminder;
 
 import net.dv8tion.jda.api.entities.Member;
-import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
 import org.togetherjava.tjbot.db.Database;
 import org.togetherjava.tjbot.db.generated.Tables;
 import org.togetherjava.tjbot.db.generated.tables.records.PendingRemindersRecord;
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindRoutineTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindRoutineTest.java
index 69aaaed1fb..2b6910eb6a 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindRoutineTest.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindRoutineTest.java
@@ -1,8 +1,13 @@
 package org.togetherjava.tjbot.commands.reminder;
 
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
 import net.dv8tion.jda.api.requests.ErrorResponse;
-import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.requests.restaction.CacheRestAction;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -49,13 +54,16 @@ private Member createAndSetupUnknownMember() {
 
         Member member = jdaTester.createMemberSpy(unknownMemberId);
 
-        RestAction unknownMemberAction = jdaTester.createFailedActionMock(
-                jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
+        CacheRestAction unknownMemberAction = jdaTester.createFailedActionMock(
+                jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER),
+                CacheRestAction.class);
         when(jdaTester.getJdaMock().retrieveUserById(unknownMemberId))
             .thenReturn(unknownMemberAction);
 
-        RestAction unknownPrivateChannelAction = jdaTester.createFailedActionMock(
-                jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
+        CacheRestAction unknownPrivateChannelAction =
+                jdaTester.createFailedActionMock(
+                        jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER),
+                        CacheRestAction.class);
         when(jdaTester.getJdaMock().openPrivateChannelById(anyLong()))
             .thenReturn(unknownPrivateChannelAction);
         when(jdaTester.getJdaMock().openPrivateChannelById(anyString()))
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java
index f2b388aa63..ce02d1a873 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagManageCommandTest.java
@@ -1,6 +1,5 @@
 package org.togetherjava.tjbot.commands.tags;
 
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.Permission;
 import net.dv8tion.jda.api.entities.Member;
 import net.dv8tion.jda.api.entities.Message;
@@ -8,6 +7,7 @@
 import net.dv8tion.jda.api.entities.Role;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.requests.ErrorResponse;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -20,11 +20,9 @@
 import org.togetherjava.tjbot.moderation.ModAuditLogWriter;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
 
@@ -133,7 +131,8 @@ private SlashCommandInteractionEvent triggerDeleteCommand(String tagId) {
     }
 
     private void postMessage(String content, String id) {
-        Message message = new MessageBuilder(content).build();
+        Message message = jdaTester.clientMessageToReceivedMessageMock(
+                new MessageCreateBuilder().setContent(content).build());
         doReturn(jdaTester.createSucceededActionMock(message)).when(jdaTester.getTextChannelSpy())
             .retrieveMessageById(id);
     }
@@ -166,7 +165,7 @@ void rawTagShowsContentIfFound() {
 
         // THEN the command responds with its content as an attachment
         verify(jdaTester.getReplyActionMock())
-            .addFile(aryEq("bar".getBytes(StandardCharsets.UTF_8)), anyString());
+            .addFiles(argThat(jdaTester.createAttachmentHasContentMatcher("bar")));
         verify(modAuditLogWriter, never()).write(any(), any(), any(), any(), any(), any());
     }
 
diff --git a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagsCommandTest.java b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagsCommandTest.java
index 870931ca49..3029377c63 100644
--- a/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagsCommandTest.java
+++ b/application/src/test/java/org/togetherjava/tjbot/commands/tags/TagsCommandTest.java
@@ -5,7 +5,6 @@
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
-import net.dv8tion.jda.api.interactions.components.ActionRow;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -44,7 +43,7 @@ private SlashCommandInteractionEvent triggerSlashCommand() {
     private ButtonInteractionEvent triggerButtonClick(Member userWhoClicked, long idOfAuthor) {
         ButtonInteractionEvent event = jdaTester.createButtonInteractionEvent()
             .setUserWhoClicked(userWhoClicked)
-            .setActionRows(ActionRow.of(TagSystem.createDeleteButton("foo")))
+            .setActionRow(TagSystem.createDeleteButton("foo"))
             .buildWithSingleButton();
         command.onButtonClick(event, List.of(Long.toString(idOfAuthor)));
         return event;
@@ -130,7 +129,7 @@ void moderatorCanDeleteList() {
     }
 
     @Test
-    @DisplayName("The list of tags can not deleted by other users")
+    @DisplayName("The list of tags cannot be deleted by other users")
     void othersCanNotDeleteList() {
         // GIVEN a '/tags' message send by an author and another user
         long idOfAuthor = 1;
diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/ButtonClickEventBuilder.java b/application/src/test/java/org/togetherjava/tjbot/jda/ButtonClickEventBuilder.java
index f336c0d5bc..dacd689b13 100644
--- a/application/src/test/java/org/togetherjava/tjbot/jda/ButtonClickEventBuilder.java
+++ b/application/src/test/java/org/togetherjava/tjbot/jda/ButtonClickEventBuilder.java
@@ -1,22 +1,20 @@
 package org.togetherjava.tjbot.jda;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import net.dv8tion.jda.api.MessageBuilder;
 import net.dv8tion.jda.api.entities.Member;
 import net.dv8tion.jda.api.entities.Message;
 import net.dv8tion.jda.api.entities.MessageEmbed;
 import net.dv8tion.jda.api.entities.User;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
-import net.dv8tion.jda.api.interactions.components.ActionRow;
+import net.dv8tion.jda.api.interactions.components.ItemComponent;
 import net.dv8tion.jda.api.interactions.components.buttons.Button;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
 import org.togetherjava.tjbot.commands.SlashCommand;
 
 import javax.annotation.Nullable;
 import java.util.List;
-import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
-import java.util.stream.Stream;
 
 import static org.mockito.Mockito.when;
 
@@ -29,12 +27,12 @@
  * Among other Discord related things, the builder optionally accepts a message
  * ({@link #setMessage(Message)}) and the user who clicked on the button
  * ({@link #setUserWhoClicked(Member)} ). As well as several ways to modify the message directly for
- * convenience, such as {@link #setContent(String)} or {@link #setActionRows(ActionRow...)}. The
+ * convenience, such as {@link #setContent(String)} or {@link #setActionRow(ItemComponent...)}. The
  * builder is by default already setup with a valid dummy message and the user who clicked the
  * button is set to the author of the message.
  * 
  * In order to build the event, at least one button has to be added to the message and marked as
- * clicked . Therefore, use {@link #setActionRows(ActionRow...)} or modify the message
+ * clicked . Therefore, use {@link #setActionRow(ItemComponent...)} or modify the message
  * manually using {@link #setMessage(Message)}. Then mark the desired button as clicked using
  * {@link #build(Button)} or, if the message only contains a single button,
  * {@link #buildWithSingleButton()} will automatically select the button.
@@ -68,9 +66,10 @@
  */
 public final class ButtonClickEventBuilder {
     private static final ObjectMapper JSON = new ObjectMapper();
+    private final JdaTester jdaTester;
     private final Supplier extends ButtonInteractionEvent> mockEventSupplier;
     private final UnaryOperator mockMessageOperator;
-    private MessageBuilder messageBuilder;
+    private MessageCreateBuilder messageBuilder;
     private Member userWhoClicked;
 
     ButtonClickEventBuilder(Supplier extends ButtonInteractionEvent> mockEventSupplier,
@@ -78,23 +77,25 @@ public final class ButtonClickEventBuilder {
         this.mockEventSupplier = mockEventSupplier;
         this.mockMessageOperator = mockMessageOperator;
 
-        messageBuilder = new MessageBuilder();
+        messageBuilder = new MessageCreateBuilder();
         messageBuilder.setContent("test message");
+
+        jdaTester = new JdaTester();
     }
 
     /**
      * Sets the given message that this event is associated to. Will override any data previously
      * set with the more direct methods such as {@link #setContent(String)} or
-     * {@link #setActionRows(ActionRow...)}.
+     * {@link #setActionRow(ItemComponent...)}.
      * 
      * The message must contain at least one button, or the button has to be added later with
-     * {@link #setActionRows(ActionRow...)}.
+     * {@link #setActionRow(ItemComponent...)}.
      *
      * @param message the message to set
      * @return this builder instance for chaining
      */
     public ButtonClickEventBuilder setMessage(Message message) {
-        messageBuilder = new MessageBuilder(message);
+        messageBuilder = MessageCreateBuilder.fromMessage(message);
         return this;
     }
 
@@ -126,13 +127,13 @@ public ButtonClickEventBuilder setEmbeds(MessageEmbed... embeds) {
      * Sets the action rows of the message that this event is associated to. Usage of
      * {@link #setMessage(Message)} will overwrite any content set by this.
      * 
-     * At least one of the rows must contain a button before {@link #build(Button)} is called.
+     * At least one of the components must be a button before {@link #build(Button)} is called.
      * 
-     * @param rows the action rows of the message
+     * @param components the components for this action row
      * @return this builder instance for chaining
      */
-    public ButtonClickEventBuilder setActionRows(ActionRow... rows) {
-        messageBuilder.setActionRows(rows);
+    public ButtonClickEventBuilder setActionRow(ItemComponent... components) {
+        messageBuilder.setActionRow(components);
         return this;
     }
 
@@ -176,10 +177,11 @@ public ButtonInteractionEvent build(Button clickedButton) {
     }
 
     private ButtonInteractionEvent createEvent(@Nullable Button maybeClickedButton) {
-        Message message = mockMessageOperator.apply(messageBuilder.build());
-        Button clickedButton = determineClickedButton(maybeClickedButton, message);
+        Message receivedMessage = mockMessageOperator
+            .apply(jdaTester.clientMessageToReceivedMessageMock(messageBuilder.build()));
+        Button clickedButton = determineClickedButton(maybeClickedButton, receivedMessage);
 
-        return mockButtonClickEvent(message, clickedButton);
+        return mockButtonClickEvent(receivedMessage, clickedButton);
     }
 
     private static Button determineClickedButton(@Nullable Button maybeClickedButton,
@@ -190,12 +192,11 @@ private static Button determineClickedButton(@Nullable Button maybeClickedButton
 
         // Otherwise, attempt to extract the button from the message. Only allow a single button in
         // this case to prevent ambiguity.
-        return requireSingleButton(getMessageButtons(message));
+        return requireSingleButton(message.getButtons());
     }
 
     private static Button requireButtonInMessage(Button clickedButton, Message message) {
-        boolean isClickedButtonUnknown =
-                getMessageButtons(message).noneMatch(clickedButton::equals);
+        boolean isClickedButtonUnknown = !message.getButtons().contains(clickedButton);
 
         if (isClickedButtonUnknown) {
             throw new IllegalArgumentException(
@@ -205,22 +206,20 @@ private static Button requireButtonInMessage(Button clickedButton, Message messa
         return clickedButton;
     }
 
-    private static Button requireSingleButton(Stream extends Button> stream) {
-        Function descriptionToException =
-                IllegalArgumentException::new;
-
-        return stream.reduce((x, y) -> {
-            throw descriptionToException
-                .apply("The message contains more than a single button, unable to automatically determine the clicked button."
-                        + " Either only use a single button or explicitly state the clicked button");
-        })
-            .orElseThrow(() -> descriptionToException.apply(
-                    "The message contains no buttons, unable to automatically determine the clicked button."
-                            + " Add the button to the message first."));
-    }
+    private static Button requireSingleButton(List extends Button> buttons) {
+        if (buttons.isEmpty()) {
+            throw new IllegalArgumentException("The message contains no buttons,"
+                    + " unable to automatically determine the clicked button."
+                    + " Add the button to the message first.");
+        }
+
+        if (buttons.size() > 1) {
+            throw new IllegalArgumentException("The message contains more than a single button,"
+                    + " unable to automatically determine the clicked button."
+                    + " Either only use a single button or explicitly state the clicked button");
+        }
 
-    private static Stream getMessageButtons(Message message) {
-        return message.getActionRows().stream().map(ActionRow::getButtons).flatMap(List::stream);
+        return buttons.get(0);
     }
 
     private ButtonInteractionEvent mockButtonClickEvent(Message message, Button clickedButton) {
diff --git a/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java b/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java
index 381d102578..7194e189de 100644
--- a/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java
+++ b/application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java
@@ -4,6 +4,11 @@
 import net.dv8tion.jda.api.JDA;
 import net.dv8tion.jda.api.Permission;
 import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
+import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
+import net.dv8tion.jda.api.entities.emoji.Emoji;
 import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
 import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
 import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
@@ -11,31 +16,40 @@
 import net.dv8tion.jda.api.interactions.InteractionHook;
 import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
 import net.dv8tion.jda.api.interactions.components.ItemComponent;
+import net.dv8tion.jda.api.interactions.components.LayoutComponent;
 import net.dv8tion.jda.api.requests.ErrorResponse;
 import net.dv8tion.jda.api.requests.Response;
 import net.dv8tion.jda.api.requests.RestAction;
+import net.dv8tion.jda.api.requests.restaction.CacheRestAction;
 import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
-import net.dv8tion.jda.api.utils.AttachmentOption;
+import net.dv8tion.jda.api.utils.AttachmentProxy;
 import net.dv8tion.jda.api.utils.ConcurrentSessionController;
+import net.dv8tion.jda.api.utils.FileUpload;
 import net.dv8tion.jda.api.utils.cache.CacheFlag;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+import net.dv8tion.jda.api.utils.messages.MessageEditData;
 import net.dv8tion.jda.internal.JDAImpl;
 import net.dv8tion.jda.internal.entities.*;
+import net.dv8tion.jda.internal.entities.channel.concrete.PrivateChannelImpl;
+import net.dv8tion.jda.internal.entities.channel.concrete.TextChannelImpl;
 import net.dv8tion.jda.internal.requests.Requester;
 import net.dv8tion.jda.internal.requests.restaction.AuditableRestActionImpl;
-import net.dv8tion.jda.internal.requests.restaction.MessageActionImpl;
-import net.dv8tion.jda.internal.requests.restaction.WebhookMessageUpdateActionImpl;
+import net.dv8tion.jda.internal.requests.restaction.MessageCreateActionImpl;
+import net.dv8tion.jda.internal.requests.restaction.WebhookMessageEditActionImpl;
 import net.dv8tion.jda.internal.requests.restaction.interactions.ReplyCallbackActionImpl;
 import net.dv8tion.jda.internal.utils.config.AuthorizationConfig;
+import org.mockito.ArgumentMatcher;
 import org.mockito.ArgumentMatchers;
+import org.mockito.MockingDetails;
 import org.mockito.stubbing.Answer;
 import org.togetherjava.tjbot.commands.SlashCommand;
 import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator;
 
 import javax.annotation.Nullable;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicLong;
@@ -44,6 +58,7 @@
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.Mockito.*;
 
@@ -86,8 +101,8 @@ public final class JdaTester {
     private final GuildImpl guild;
     private final ReplyCallbackActionImpl replyAction;
     private final AuditableRestActionImpl auditableRestAction;
-    private final MessageActionImpl messageAction;
-    private final WebhookMessageUpdateActionImpl webhookMessageUpdateAction;
+    private final MessageCreateActionImpl messageCreateAction;
+    private final WebhookMessageEditActionImpl webhookMessageEditAction;
     private final TextChannelImpl textChannel;
     private final PrivateChannelImpl privateChannel;
     private final InteractionHook interactionHook;
@@ -115,8 +130,8 @@ public JdaTester() {
         member = spy(new MemberImpl(guild, user));
         textChannel = spy(new TextChannelImpl(TEXT_CHANNEL_ID, guild));
         privateChannel = spy(new PrivateChannelImpl(jda, PRIVATE_CHANNEL_ID, user));
-        messageAction = mock(MessageActionImpl.class);
-        webhookMessageUpdateAction = mock(WebhookMessageUpdateActionImpl.class);
+        messageCreateAction = mock(MessageCreateActionImpl.class);
+        webhookMessageEditAction = mock(WebhookMessageEditActionImpl.class);
         replyCallbackAction = mock(ReplyCallbackAction.class);
         EntityBuilder entityBuilder = mock(EntityBuilder.class);
         Role everyoneRole = new RoleImpl(GUILD_ID, guild);
@@ -152,48 +167,52 @@ public JdaTester() {
         when(replyAction.addActionRow(ArgumentMatchers.any()))
             .thenReturn(replyAction);
         when(replyAction.setContent(anyString())).thenReturn(replyAction);
-        when(replyAction.addFile(any(byte[].class), any(String.class), any(AttachmentOption.class)))
-            .thenReturn(replyAction);
+        when(replyAction.addFiles(anyCollection())).thenReturn(replyAction);
+        when(replyAction.addFiles(any(FileUpload.class))).thenReturn(replyAction);
         doNothing().when(replyAction).queue();
 
         auditableRestAction = createSucceededActionMock(null, AuditableRestActionImpl.class);
 
-        doNothing().when(webhookMessageUpdateAction).queue();
-        doReturn(webhookMessageUpdateAction).when(webhookMessageUpdateAction)
+        doNothing().when(webhookMessageEditAction).queue();
+        doReturn(webhookMessageEditAction).when(webhookMessageEditAction)
             .setActionRow(any(ItemComponent.class));
 
         doReturn(everyoneRole).when(guild).getPublicRole();
         doReturn(selfMember).when(guild).getMember(selfUser);
         doReturn(member).when(guild).getMember(not(eq(selfUser)));
 
-        RestAction userAction = createSucceededActionMock(member.getUser(), RestAction.class);
+        CacheRestAction userAction =
+                createSucceededActionMock(member.getUser(), CacheRestAction.class);
         when(jda.retrieveUserById(anyLong())).thenReturn(userAction);
 
         doReturn(null).when(textChannel).retrieveMessageById(any());
 
         interactionHook = mock(InteractionHook.class);
-        when(interactionHook.editOriginal(anyString())).thenReturn(webhookMessageUpdateAction);
-        when(interactionHook.editOriginal(any(Message.class)))
-            .thenReturn(webhookMessageUpdateAction);
-        when(interactionHook.editOriginal(any(byte[].class), any(), any()))
-            .thenReturn(webhookMessageUpdateAction);
-
-        doReturn(messageAction).when(textChannel).sendMessageEmbeds(any(), any());
-        doReturn(messageAction).when(textChannel).sendMessageEmbeds(any());
-
-        doNothing().when(messageAction).queue();
-        when(messageAction.content(any())).thenReturn(messageAction);
-
-        RestAction privateChannelAction =
-                createSucceededActionMock(privateChannel, RestAction.class);
+        when(interactionHook.editOriginal(anyString())).thenReturn(webhookMessageEditAction);
+        when(interactionHook.editOriginal(any(MessageEditData.class)))
+            .thenReturn(webhookMessageEditAction);
+        when(interactionHook.editOriginalAttachments(any(FileUpload.class)))
+            .thenReturn(webhookMessageEditAction);
+
+        doReturn(messageCreateAction).when(textChannel).sendMessageEmbeds(any(), any());
+        doReturn(messageCreateAction).when(textChannel).sendMessageEmbeds(any());
+        doReturn(privateChannel).when(textChannel).asPrivateChannel();
+
+        doNothing().when(messageCreateAction).queue();
+        when(messageCreateAction.setContent(any())).thenReturn(messageCreateAction);
+        when(messageCreateAction.addContent(any())).thenReturn(messageCreateAction);
+
+        CacheRestAction privateChannelAction =
+                createSucceededActionMock(privateChannel, CacheRestAction.class);
         when(jda.openPrivateChannelById(anyLong())).thenReturn(privateChannelAction);
         when(jda.openPrivateChannelById(anyString())).thenReturn(privateChannelAction);
         doReturn(privateChannelAction).when(user).openPrivateChannel();
         doReturn(null).when(privateChannel).retrieveMessageById(any());
-        doReturn(messageAction).when(privateChannel).sendMessage(anyString());
-        doReturn(messageAction).when(privateChannel).sendMessage(any(Message.class));
-        doReturn(messageAction).when(privateChannel).sendMessageEmbeds(any(), any());
-        doReturn(messageAction).when(privateChannel).sendMessageEmbeds(any());
+        doReturn(messageCreateAction).when(privateChannel).sendMessage(anyString());
+        doReturn(messageCreateAction).when(privateChannel)
+            .sendMessage(any(MessageCreateData.class));
+        doReturn(messageCreateAction).when(privateChannel).sendMessageEmbeds(any(), any());
+        doReturn(messageCreateAction).when(privateChannel).sendMessageEmbeds(any());
     }
 
     /**
@@ -240,7 +259,10 @@ public ButtonClickEventBuilder createButtonInteractionEvent() {
         };
 
         UnaryOperator mockMessageOperator = event -> {
-            Message message = spy(event);
+            MockingDetails mockingDetails = mockingDetails(event);
+            Message message =
+                    mockingDetails.isMock() || mockingDetails.isSpy() ? event : spy(event);
+
             mockMessage(message);
             return message;
         };
@@ -532,13 +554,63 @@ public ErrorResponseException createErrorResponseException(ErrorResponse reason)
      * @param attachments attachments of the message, empty if none
      * @return the event of receiving the given message
      */
-    public MessageReceivedEvent createMessageReceiveEvent(Message message,
+    public MessageReceivedEvent createMessageReceiveEvent(MessageCreateData message,
             List attachments) {
-        Message spyMessage = spy(message);
-        mockMessage(spyMessage);
-        doReturn(attachments).when(spyMessage).getAttachments();
+        Message receivedMessage = clientMessageToReceivedMessageMock(message);
+        mockMessage(receivedMessage);
+        doReturn(attachments).when(receivedMessage).getAttachments();
+
+        return new MessageReceivedEvent(jda, responseNumber.getAndIncrement(), receivedMessage);
+    }
+
+    /**
+     * Creates an argument matcher that asserts that an attachment has the given content.
+     * 
+     * This requires the data-stream in the attachment to support
+     * {@link InputStream#markSupported()}. This is the case for most simpler streams, such as
+     * strings or files.
+     * 
+     * An example would be
+     * 
+     * 
+     * {
+     *     @code
+     *     verify(jdaTester.getReplyActionMock())
+     *       .addFiles(
+     *         argThat(jdaTester.createAttachmentHasContentMatcher("foo"))
+     *       );
+     *
+     *     // checking that the following has been called
+     *
+     *     event.reply("")
+     *       .addFiles(
+     *         FileUpload.fromData("foo".getBytes(StandardCharsets.UTF_8), "")
+     *       );
+     * }
+     *  
+     *
+     * @param content the content the attachment should have
+     * @return the created matcher
+     */
+    public ArgumentMatcher createAttachmentHasContentMatcher(String content) {
+        return attachment -> {
+            if (attachment == null) {
+                return false;
+            }
+
+            InputStream dataStream = attachment.getData();
+            if (!dataStream.markSupported()) {
+                return false;
+            }
+
+            byte[] expectedContentRaw = content.getBytes(StandardCharsets.UTF_8);
+            dataStream.mark(expectedContentRaw.length);
+
+            byte[] actualContent = assertDoesNotThrow(() -> attachment.getData().readAllBytes());
+            assertDoesNotThrow(dataStream::reset);
 
-        return new MessageReceivedEvent(jda, responseNumber.getAndIncrement(), spyMessage);
+            return Arrays.equals(expectedContentRaw, actualContent);
+        };
     }
 
     private void mockInteraction(IReplyCallback interaction) {
@@ -551,9 +623,7 @@ private void mockInteraction(IReplyCallback interaction) {
 
         doReturn(textChannel).when(interaction).getChannel();
         doReturn(textChannel).when(interaction).getMessageChannel();
-        doReturn(textChannel).when(interaction).getTextChannel();
         doReturn(textChannel).when(interaction).getGuildChannel();
-        doReturn(privateChannel).when(interaction).getPrivateChannel();
 
         doReturn(interactionHook).when(interaction).getHook();
         doReturn(replyCallbackAction).when(interaction).deferReply();
@@ -567,14 +637,14 @@ private void mockButtonClickEvent(ButtonInteractionEvent event) {
     }
 
     private void mockMessage(Message message) {
-        doReturn(messageAction).when(message).reply(anyString());
-        doReturn(messageAction).when(message).replyEmbeds(ArgumentMatchers.any());
-        doReturn(messageAction).when(message).replyEmbeds(anyCollection());
+        doReturn(messageCreateAction).when(message).reply(anyString());
+        doReturn(messageCreateAction).when(message)
+            .replyEmbeds(ArgumentMatchers.any());
+        doReturn(messageCreateAction).when(message).replyEmbeds(anyCollection());
 
         doReturn(auditableRestAction).when(message).delete();
 
-        doReturn(auditableRestAction).when(message).addReaction(any(Emote.class));
-        doReturn(auditableRestAction).when(message).addReaction(any(String.class));
+        doReturn(auditableRestAction).when(message).addReaction(any(Emoji.class));
 
         doReturn(member).when(message).getMember();
         doReturn(member.getUser()).when(message).getAuthor();
@@ -586,4 +656,61 @@ private void mockMessage(Message message) {
         doReturn(message.getContentRaw()).when(message).getContentDisplay();
         doReturn(message.getContentRaw()).when(message).getContentStripped();
     }
+
+    /**
+     * Transforms the given client-side message to a mocked message received from Discord.
+     *
+     * @param clientMessage the client-side message to transform
+     * @return the mocked copy of the given message, but as message received from Discord
+     */
+    public Message clientMessageToReceivedMessageMock(MessageCreateData clientMessage) {
+        Message receivedMessage = mock(Message.class);
+        var foo = clientMessage.getComponents();
+
+        when(receivedMessage.getJDA()).thenReturn(jda);
+        when(receivedMessage.getEmbeds()).thenReturn(clientMessage.getEmbeds());
+        when(receivedMessage.getContentRaw()).thenReturn(clientMessage.getContent());
+        when(receivedMessage.getContentDisplay()).thenReturn(clientMessage.getContent());
+        when(receivedMessage.getContentStripped()).thenReturn(clientMessage.getContent());
+
+        when(receivedMessage.getComponents()).thenReturn(clientMessage.getComponents());
+        when(receivedMessage.getButtons()).thenReturn(clientMessage.getComponents()
+            .stream()
+            .map(LayoutComponent::getButtons)
+            .flatMap(List::stream)
+            .toList());
+
+        List attachments = clientMessage.getAttachments()
+            .stream()
+            .map(this::clientAttachmentToReceivedAttachmentMock)
+            .toList();
+        when(receivedMessage.getAttachments()).thenReturn(attachments);
+
+        return receivedMessage;
+    }
+
+    private Message.Attachment clientAttachmentToReceivedAttachmentMock(
+            FileUpload clientAttachment) {
+        Message.Attachment receivedAttachment = mock(Message.Attachment.class);
+        AttachmentProxy attachmentProxy = mock(AttachmentProxy.class);
+
+        when(receivedAttachment.getJDA()).thenReturn(jda);
+        when(receivedAttachment.getFileName()).thenReturn(clientAttachment.getName());
+        when(receivedAttachment.getFileExtension())
+            .thenReturn(getFileExtension(clientAttachment.getName()).orElse(null));
+        when(receivedAttachment.getProxy()).thenReturn(attachmentProxy);
+
+        when(attachmentProxy.download())
+            .thenReturn(CompletableFuture.completedFuture(clientAttachment.getData()));
+
+        return receivedAttachment;
+    }
+
+    private static Optional getFileExtension(String fileName) {
+        int extensionStartIndex = fileName.lastIndexOf('.');
+        if (extensionStartIndex == -1) {
+            return Optional.empty();
+        }
+        return Optional.of(fileName.substring(extensionStartIndex + 1));
+    }
 }