Skip to content

delete blacklisted attachments from message and replace with webhook #520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

/**
Expand All @@ -37,6 +38,7 @@ public class GuildConfig {
private StarboardConfig starboardConfig;
private MessageCacheConfig messageCacheConfig;
private ServerLockConfig serverLockConfig;
private List<String> blacklistedMessageExtensions;

/**
* Constructor that initializes all Config classes.
Expand All @@ -54,6 +56,8 @@ public GuildConfig(Guild guild, Path file) {
this.starboardConfig = new StarboardConfig();
this.messageCacheConfig = new MessageCacheConfig();
this.serverLockConfig = new ServerLockConfig();
this.blacklistedMessageExtensions = List.of("application/x-msdos-program", "application/java-archive",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for the use of MIME types instead of extensions?
If you are using MIME types, why is the variable called "extensions"?

"application/zip");
this.setGuild(guild);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.discordjug.javabot.listener;

import lombok.RequiredArgsConstructor;
import net.discordjug.javabot.data.config.BotConfig;
import net.discordjug.javabot.data.config.GuildConfig;
import net.discordjug.javabot.util.ExceptionLogger;
import net.discordjug.javabot.util.WebhookUtil;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.attribute.IWebhookContainer;
import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
public class BlacklistedMessageAttachmentListener extends ListenerAdapter {

private final BotConfig botConfig;

@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
if (event.getAuthor().isBot() || event.getAuthor().isSystem()) return;
GuildConfig guildConfig = botConfig.get(event.getGuild());
List<String> blacklistedMessageExtensions = guildConfig.getBlacklistedMessageExtensions();
Message message = event.getMessage();
List<Message.Attachment> attachments = message.getAttachments();
List<Message.Attachment> allowedAttachments = new ArrayList<>();
attachments.forEach(attachment -> {
Copy link
Member

@danthe1st danthe1st Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done nicely using the stream API, for example

List<Message.Attachment> allowedAttachments = attachments
    .stream()
    .filter(this::isAllowedAttachment)
    .toList();

boolean contains = blacklistedMessageExtensions.contains(attachment.getContentType());
if (!contains) {
allowedAttachments.add(attachment);
}
});
if (message.getAttachments().size() != allowedAttachments.size()) {
IWebhookContainer tc = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tc would imply TextChannel. webhookContainer (or something else if you have a better idea) would be a better name.

if (event.isFromType(ChannelType.TEXT)) {
tc = event.getChannel().asTextChannel();
}
if (event.isFromThread()) {
StandardGuildChannel parentChannel = event.getChannel()
.asThreadChannel()
.getParentChannel()
.asStandardGuildChannel();
tc = (IWebhookContainer) parentChannel;
}
if (tc == null) {
return;
}
long threadId = event.isFromThread() ? event.getChannel().getIdLong() : 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can be merged with the previous event.isFromThread() check by declaring the variable before (and initializing it with 0).

WebhookUtil.ensureWebhookExists(
tc,
wh -> WebhookUtil.replaceMemberMessageWithAttachments(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a small embed to the message saying that unwanted attachments were removed.

wh,
event.getMessage(),
event.getMessage().getContentRaw(),
threadId,
allowedAttachments
),
e -> ExceptionLogger.capture(
e,
"Error creating webhook for UnformattedCodeListener"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is incorrect here.

)
);
}
}
}
71 changes: 71 additions & 0 deletions src/main/java/net/discordjug/javabot/util/WebhookUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,59 @@ private static CompletableFuture<ReadonlyMessage> mirrorMessageToWebhook(@NotNul
});
}

/**
* Resends a specific message using a webhook with a custom content.
*
* @param webhook the webhook used for sending the message
* @param originalMessage the message to copy
* @param newMessageContent the new (custom) content
* @param threadId the thread to send the message in or {@code 0} if the
* message should be sent directly
* @param components A nullable list of {@link LayoutComponent}s.
* @param attachments A list of {@link Message.Attachment}s.
* @return a {@link CompletableFuture} representing the action of sending
* the message
*/
public static CompletableFuture<ReadonlyMessage> mirrorMessageToWebhookWithAttachments(@NotNull Webhook webhook, @NotNull Message originalMessage, String newMessageContent, long threadId, @Nullable List<LayoutComponent> components, List<Message.Attachment> attachments) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't duplicate code. Instead, you can add an attachments parameter to the existing method and create a new overload with the old signature.

return originalMessage
.getGuild()
.retrieveMember(originalMessage.getAuthor())
.submit()
.exceptionally(e -> null)//if the member cannot be found, use no member information
.thenCompose(member ->
mirrorMessageToWebhookWithAttachments(webhook, originalMessage, newMessageContent, threadId, components, attachments, member));
}

private static CompletableFuture<ReadonlyMessage> mirrorMessageToWebhookWithAttachments(@NotNull Webhook webhook, Message originalMessage, String newMessageContent, long threadId,
List<LayoutComponent> components, List<Message.Attachment> attachments, Member member) {
WebhookMessageBuilder message = new WebhookMessageBuilder().setContent(newMessageContent)
.setAllowedMentions(AllowedMentions.none())
.setAvatarUrl(transformOrNull(member, Member::getEffectiveAvatarUrl))
.setUsername(transformOrNull(member, Member::getEffectiveName));
JDAWebhookClient client = new WebhookClientBuilder(webhook.getIdLong(), webhook.getToken()).setThreadId(threadId)
.buildJDA();
if (components != null && !components.isEmpty()) {
message.addComponents(components);
}

@SuppressWarnings("unchecked")
CompletableFuture<?>[] futures = new CompletableFuture<?>[attachments.size()];
for (int i = 0; i < attachments.size(); i++) {
Attachment attachment = attachments.get(i);
futures[i] = attachment.getProxy()
.download()
.thenAccept(is -> message.addFile((attachment.isSpoiler() ? "SPOILER_" : "") + attachment.getFileName(), is));
}
return CompletableFuture.allOf(futures)
.thenCompose(unused -> sendMessage(client, message))
.whenComplete((result, err) -> {
client.close();
if (err != null) {
ExceptionLogger.capture(err, WebhookUtil.class.getSimpleName());
}
});
}

private static <T, R> R transformOrNull(T toTransform, Function<T, R> transformer) {
return toTransform == null ? null : transformer.apply(toTransform);
}
Expand Down Expand Up @@ -165,4 +218,22 @@ public static void replaceMemberMessage(Webhook webhook, Message originalMessage
return null;
});
}

/**
* Method for replacing a user's guild message through a webhook while also replacing the attachments.
*
* @param webhook a reference to a webhook
* @param originalMessage a reference to the {@link Message} object that should be replaced
* @param newMessageContent a String containing the new message's content
* @param threadId id of the thread in which the message should be replaced
* @param attachments attachments to be added to the message
*/
public static void replaceMemberMessageWithAttachments(Webhook webhook, Message originalMessage, String newMessageContent, long threadId, List<Message.Attachment> attachments) {
WebhookUtil.mirrorMessageToWebhookWithAttachments(webhook, originalMessage, newMessageContent, threadId, null, attachments)
.thenAccept(unused -> originalMessage.delete().queue())
.exceptionally(e -> {
ExceptionLogger.capture(e, WebhookUtil.class.getSimpleName());
return null;
});
}
}
Loading