From 0f11167953a36bad7d23fbe8b9c1eb053140a023 Mon Sep 17 00:00:00 2001 From: akastijn Date: Fri, 21 Nov 2025 23:39:35 +0100 Subject: [PATCH] Refactor Discord message sending to use `MessageForEmbed` object and add support for creating threads in targeted channels. --- .../services/discord/AppealDiscord.java | 13 +-- .../discord/StaffApplicationDiscord.java | 10 +-- .../webinterface/objects/MessageForEmbed.java | 49 ++++++++++ .../send_message/DiscordSender.java | 90 +++++++++++-------- 4 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 discord/src/main/java/com/alttd/webinterface/objects/MessageForEmbed.java diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java b/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java index 2739a6f..e941307 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/discord/AppealDiscord.java @@ -9,6 +9,7 @@ import com.alttd.altitudeweb.database.litebans.HistoryType; import com.alttd.altitudeweb.database.litebans.UserType; import com.alttd.altitudeweb.database.web_db.forms.Appeal; import com.alttd.altitudeweb.setup.Connection; +import com.alttd.webinterface.objects.MessageForEmbed; import com.alttd.webinterface.send_message.DiscordSender; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -97,15 +98,9 @@ public class AppealDiscord { // colorRgb = null (use default), timestamp = appeal.createdAt if available Instant timestamp = appeal.createdAt() != null ? appeal.createdAt() : Instant.now(); - DiscordSender.getInstance().sendEmbedToChannels( - channelIds, - "New Appeal Submitted", - description, - fields, - null, - timestamp, - null - ); + MessageForEmbed newAppealSubmitted = new MessageForEmbed( + "New Appeal Submitted", description, fields, null, timestamp, null); + DiscordSender.getInstance().sendEmbedWithThreadToChannels(channelIds, newAppealSubmitted, "Appeal"); } private CompletableFuture getCountAsync(HistoryType type, java.util.UUID uuid) { diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java b/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java index 7177907..7af9c58 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/discord/StaffApplicationDiscord.java @@ -5,6 +5,7 @@ import com.alttd.altitudeweb.database.discord.OutputChannel; import com.alttd.altitudeweb.database.discord.OutputChannelMapper; import com.alttd.altitudeweb.database.web_db.forms.StaffApplication; import com.alttd.altitudeweb.setup.Connection; +import com.alttd.webinterface.objects.MessageForEmbed; import com.alttd.webinterface.send_message.DiscordSender; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -79,16 +80,15 @@ public class StaffApplicationDiscord { .toList(); Instant timestamp = application.createdAt() != null ? application.createdAt() : Instant.now(); - DiscordSender.getInstance().sendEmbedToChannels( - channelIds, + MessageForEmbed messageForEmbed = new MessageForEmbed( "New Staff Application Submitted", "Join date: " + (application.joinDate() != null ? application.joinDate().toString() : "unknown") + - "\nSubmitted: " + formatInstant(timestamp), + "\nSubmitted: " + formatInstant(timestamp), fields, null, timestamp, - null - ); + null); + DiscordSender.getInstance().sendEmbedWithThreadToChannels(channelIds, messageForEmbed, "Staff Application"); } private String safe(String s) { diff --git a/discord/src/main/java/com/alttd/webinterface/objects/MessageForEmbed.java b/discord/src/main/java/com/alttd/webinterface/objects/MessageForEmbed.java new file mode 100644 index 0000000..e7a821c --- /dev/null +++ b/discord/src/main/java/com/alttd/webinterface/objects/MessageForEmbed.java @@ -0,0 +1,49 @@ +package com.alttd.webinterface.objects; + +import com.alttd.webinterface.send_message.DiscordSender; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.awt.*; +import java.time.Instant; +import java.util.List; + +public record MessageForEmbed(String title, String description, List fields, Integer colorRgb, + Instant timestamp, String footer) { + + public MessageEmbed toEmbed() { + EmbedBuilder eb = new EmbedBuilder(); + if (title != null && !title.isBlank()) { + eb.setTitle(title); + } + if (description != null && !description.isBlank()) { + eb.setDescription(description); + } + if (colorRgb != null) { + eb.setColor(new Color(colorRgb)); + } else { + eb.setColor(new Color(0xFF8C00)); // default orange + } + eb.setTimestamp(timestamp != null ? timestamp : Instant.now()); + if (footer != null && !footer.isBlank()) { + eb.setFooter(footer); + } + + if (fields != null) { + for (DiscordSender.EmbedField f : fields) { + if (f == null) { + continue; + } + String name = f.getName() == null ? "" : f.getName(); + String value = f.getValue() == null ? "" : f.getValue(); + // JDA field value max is 1024; truncate to be safe + if (value.length() > 1024) { + value = value.substring(0, 1021) + "..."; + } + eb.addField(new MessageEmbed.Field(name, value, f.isInline())); + } + } + + return eb.build(); + } +} diff --git a/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java b/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java index 5beb185..74ce3f2 100644 --- a/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java +++ b/discord/src/main/java/com/alttd/webinterface/send_message/DiscordSender.java @@ -1,19 +1,21 @@ package com.alttd.webinterface.send_message; import com.alttd.webinterface.bot.DiscordBotInstance; +import com.alttd.webinterface.objects.MessageForEmbed; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; -import java.awt.*; -import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; @Slf4j public class DiscordSender { @@ -70,12 +72,38 @@ public class DiscordSender { }); } - public void sendEmbedToChannels(List channelIds, String title, String description, List fields, - Integer colorRgb, Instant timestamp, String footer) { + public void sendEmbedWithThreadToChannels(List channelIds, MessageForEmbed messageForEmbed, String threadName) { + sendEmbedToChannels(channelIds, messageForEmbed).whenCompleteAsync((result, error) -> { + if (error != null) { + log.error("Failed sending embed to channels", error); + return; + } + result.stream() + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(message -> { + message.createThreadChannel(threadName).queue(); + }); + }); + } + + public CompletableFuture>> sendEmbedToChannels(List channelIds, MessageForEmbed messageForEmbed) { + List>> futures = new ArrayList<>(); + for (Long channelId : channelIds) { + futures.add(sendEmbedToChannel(channelId, messageForEmbed)); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> + futures.stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); + } + + public CompletableFuture> sendEmbedToChannel(Long channelId, MessageForEmbed messageForEmbed) { ensureStarted(); if (botInstance.getJda() == null) { log.error("JDA not initialized; cannot send Discord embed."); - return; + return CompletableFuture.completedFuture(Optional.empty()); } try { if (!botInstance.isReady()) { @@ -83,43 +111,31 @@ public class DiscordSender { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); + return CompletableFuture.completedFuture(Optional.empty()); } catch (Exception e) { log.warn("Error while waiting for JDA ready state", e); + return CompletableFuture.completedFuture(Optional.empty()); } - EmbedBuilder eb = new EmbedBuilder(); - if (title != null && !title.isBlank()) eb.setTitle(title); - if (description != null && !description.isBlank()) eb.setDescription(description); - if (colorRgb != null) eb.setColor(new Color(colorRgb)); else eb.setColor(new Color(0xFF8C00)); // default orange - eb.setTimestamp(timestamp != null ? timestamp : Instant.now()); - if (footer != null && !footer.isBlank()) eb.setFooter(footer); + MessageEmbed embed = messageForEmbed.toEmbed(); - if (fields != null) { - for (EmbedField f : fields) { - if (f == null) continue; - String name = f.getName() == null ? "" : f.getName(); - String value = f.getValue() == null ? "" : f.getValue(); - // JDA field value max is 1024; truncate to be safe - if (value.length() > 1024) value = value.substring(0, 1021) + "..."; - eb.addField(new MessageEmbed.Field(name, value, f.isInline())); - } + TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, channelId); + if (channel == null) { + log.warn("TextChannel with id {} not found when sending embed message", channelId); + return CompletableFuture.completedFuture(Optional.empty()); } - - MessageEmbed embed = eb.build(); - - channelIds.stream() - .filter(Objects::nonNull) - .forEach(id -> { - TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id); - if (channel == null) { - log.warn("TextChannel with id {} not found", id); - return; - } - channel.sendMessageEmbeds(embed).queue( - success -> log.debug("Sent embed to channel {}", id), - error -> log.error("Failed sending embed to channel {}", id, error) - ); - }); + CompletableFuture> completableFuture = new CompletableFuture<>(); + channel.sendMessageEmbeds(embed).queue( + message -> { + completableFuture.complete(Optional.of(message)); + log.debug("Sent embed to channel {}", channelId); + }, + error -> { + completableFuture.complete(Optional.empty()); + log.error("Failed sending embed to channel {}", channelId, error); + } + ); + return completableFuture; } @Data