From 8e0f29fc02b60fe22fd4c9b3bfa0c1fac1b423a3 Mon Sep 17 00:00:00 2001 From: Neil Tomar Date: Fri, 17 Jul 2026 21:52:39 +0530 Subject: [PATCH 1/2] Add cross-channel spam automod --- .../data/config/guild/ModerationConfig.java | 12 +++++ .../message_cache/MessageCacheListener.java | 4 +- .../dao/MessageCacheRepository.java | 3 +- .../message_cache/model/CachedMessage.java | 10 ++-- .../javabot/systems/moderation/AutoMod.java | 47 ++++++++++++++++--- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java b/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java index fff66c10f..575895572 100644 --- a/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java +++ b/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java @@ -28,6 +28,18 @@ public class ModerationConfig extends GuildConfigItem { private long adminRoleId = 0; private long expertRoleId = 0; + /** + * The time window, in seconds, that the cross-channel spam automod looks back over when + * counting a user's recent messages. If this is {@code 0}, the cross-channel spam automod + * is disabled. + */ + private int crossChannelSpamWindowSeconds = 0; + + /** + * The number of distinct channels a user must post in within + * {@link #crossChannelSpamWindowSeconds} seconds before the cross-channel spam automod acts. + */ + private int crossChannelSpamMinChannels = 3; /** * ID of the share-knowledge channel. */ diff --git a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCacheListener.java b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCacheListener.java index 061f1c5be..522929f0b 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCacheListener.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCacheListener.java @@ -39,10 +39,10 @@ public void onMessageUpdate(@NotNull MessageUpdateEvent event) { CachedMessage before; if (optional.isPresent()) { CachedMessage inCache= optional.get(); - before = new CachedMessage(inCache.getMessageId(), inCache.getAuthorId(), inCache.getMessageContent(), inCache.getAttachments()); + before = new CachedMessage(inCache.getMessageId(), inCache.getAuthorId(), inCache.getChannelId(),inCache.getMessageContent(), inCache.getAttachments()); inCache.init(event.getMessage()); } else { - before = new CachedMessage(event.getMessageIdLong(), event.getAuthor().getIdLong(), "[unknown content]", List.of()); + before = new CachedMessage(event.getMessageIdLong(), event.getAuthor().getIdLong(), event.getChannel().getIdLong(),"[unknown content]", List.of()); messageCache.cache(event.getMessage()); } messageCache.sendUpdatedMessageToLog(event.getMessage(), before); diff --git a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/dao/MessageCacheRepository.java b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/dao/MessageCacheRepository.java index e86f1408e..b28a110d2 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/dao/MessageCacheRepository.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/dao/MessageCacheRepository.java @@ -88,7 +88,7 @@ public List getAll() throws DataAccessException { messages.merge(msg.getMessageId(), msg, (oldValue, value) -> { ArrayList attachments = new ArrayList<>(oldValue.getAttachments()); attachments.addAll(value.getAttachments()); - return new CachedMessage(oldValue.getMessageId(), oldValue.getAuthorId(), oldValue.getMessageContent(), attachments); + return new CachedMessage(oldValue.getMessageId(), oldValue.getAuthorId(), oldValue.getChannelId(),oldValue.getMessageContent(), attachments); }); } return new ArrayList<>(messages.values()); @@ -119,6 +119,7 @@ private CachedMessage read(ResultSet rs) throws SQLException { return new CachedMessage( rs.getLong("message_cache.message_id"), rs.getLong("author_id"), + rs.getLong("channel_id"), rs.getString("message_content"), attachments); } diff --git a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java index 2d7976a28..b6a445dd0 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java @@ -9,6 +9,7 @@ import net.discordjug.javabot.util.MessageUtils; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message.Attachment; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; /** * Represents a cached Message. @@ -19,12 +20,14 @@ public class CachedMessage { private final long messageId; private final long authorId; + private final long channelId; private String messageContent; private List attachments=new ArrayList<>(); - private CachedMessage(long messageId, long authorId) { + private CachedMessage(long messageId, long authorId,long channelId) { this.messageId = messageId; this.authorId = authorId; + this.channelId = channelId; } /** @@ -34,10 +37,11 @@ private CachedMessage(long messageId, long authorId) { * @param messageContent the textual content of the message * @param attachments The attachment URLs */ - public CachedMessage(long messageId, long authorId, String messageContent, List attachments) { + public CachedMessage(long messageId, long authorId, long channelId, String messageContent, List attachments) { super(); this.messageId = messageId; this.authorId = authorId; + this.channelId = channelId; this.messageContent = messageContent; this.attachments = List.copyOf(attachments); } @@ -49,7 +53,7 @@ public CachedMessage(long messageId, long authorId, String messageContent, List< * @return The built {@link CachedMessage}. */ public static CachedMessage of(Message message) { - CachedMessage cachedMessage = new CachedMessage(message.getIdLong(), message.getAuthor().getIdLong()); + CachedMessage cachedMessage = new CachedMessage(message.getIdLong(), message.getAuthor().getIdLong(),message.getChannelIdLong()); cachedMessage.init(message); return cachedMessage; } diff --git a/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java b/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java index f04ab922d..d5acf1c1b 100644 --- a/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java +++ b/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java @@ -2,12 +2,15 @@ import lombok.extern.slf4j.Slf4j; import net.discordjug.javabot.data.config.BotConfig; +import net.discordjug.javabot.data.config.guild.ModerationConfig; import net.discordjug.javabot.data.h2db.message_cache.MessageCache; +import net.discordjug.javabot.data.h2db.message_cache.model.CachedMessage; import net.discordjug.javabot.systems.moderation.warn.model.WarnSeverity; import net.discordjug.javabot.systems.notification.NotificationService; import net.discordjug.javabot.util.ExceptionLogger; import net.discordjug.javabot.util.MessageUtils; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; @@ -24,6 +27,7 @@ import java.net.URL; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Scanner; @@ -50,7 +54,7 @@ public class AutoMod extends ListenerAdapter { private final MessageCache messageCache; /** - * Constructor of the class, that creates a list of strings with potential spam/scam urls. + * Constructor of the class, that creates a list of strings with potential spam/scam URLs. * @param notificationService The {@link QOTWPointsService} * @param botConfig The main configuration of the bot * @param moderationService Service object for moderating members @@ -108,18 +112,33 @@ private void checkNewMessageAutomod(@Nonnull Message message) { .stream() .filter(cached -> cached.getMessageId() != message.getIdLong()) // exclude new/current message .filter(cached -> cached.getAuthorId() == message.getAuthor().getIdLong()) - .filter(cached -> + .filter(cached -> // only java files -> not spam - cached.getAttachments().isEmpty() || + cached.getAttachments().isEmpty() || cached.getAttachments().stream() .anyMatch(attachment -> !attachment.contains(".java?"))) .count() + 1; // include new message - + if (spamCount >= 5) { handleSpam(message, message.getMember()); } checkContentAutomod(message); + checkCrossChannelSpam(message); + } + + private void checkCrossChannelSpam(@Nonnull Message message) { + List spamMessages = new ArrayList<>(); + messageCache.getMessagesAfter(message.getTimeCreated().minusSeconds((botConfig.get(message.getGuild()).getModerationConfig()).getCrossChannelSpamWindowSeconds())).stream() + .filter(cachedMessage -> cachedMessage.getAuthorId() == message.getAuthor().getIdLong()) + .filter(cachedMessage -> spamMessages.stream().noneMatch( + spamMessage -> spamMessage.getChannelId() == cachedMessage.getChannelId())) + .forEach(spamMessages::add); + + if(spamMessages.size() > (botConfig.get(message.getGuild()).getModerationConfig()).getCrossChannelSpamMinChannels()){ + handleSpam(spamMessages,message); + } + } /** @@ -174,6 +193,22 @@ private void handleSpam(@Nonnull Message msg, Member member) { msg.delete().queue(); } + private void handleSpam(@Nonnull List cachedMessages, Message message) { + Guild guild = message.getGuild(); + moderationService + .timeout( + message.getAuthor(), + "Automod: Spam", + message.getGuild().getSelfMember(), + Duration.of(6, ChronoUnit.HOURS), + message.getChannel(), + false + ); + + cachedMessages.forEach(cachedMessage -> guild.getTextChannelById(cachedMessage.getChannelId()) + .deleteMessageById(cachedMessage.getMessageId()).queue()); + } + /** * returns the original String cleaned up of unused code points and spaces. * @@ -218,7 +253,7 @@ public boolean hasSuspiciousLink(@NotNull Message message) { * Checks whether the given message contains a discord invite link. * * @param message The Message to check. - * @return True if an invite is found and False if not. + * @return True if an invitation is found and False if not. */ public boolean hasAdvertisingLink(@NotNull Message message) { // Advertising @@ -237,5 +272,5 @@ private boolean isSuggestionsChannel(@NotNull MessageChannelUnion channel) { return channel.getType().isGuild() && channel.getIdLong() == botConfig.get(channel.asGuildMessageChannel().getGuild()).getModerationConfig().getSuggestionChannel().getIdLong(); } - + } \ No newline at end of file From a7a4d52414f707f8964b127168da4d1c59b03ab6 Mon Sep 17 00:00:00 2001 From: Neil Tomar Date: Fri, 17 Jul 2026 22:25:39 +0530 Subject: [PATCH 2/2] fix small issues --- .../javabot/data/h2db/message_cache/model/CachedMessage.java | 2 +- .../java/net/discordjug/javabot/systems/moderation/AutoMod.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java index b6a445dd0..cce1cd483 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java @@ -9,7 +9,6 @@ import net.discordjug.javabot.util.MessageUtils; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message.Attachment; -import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; /** * Represents a cached Message. @@ -34,6 +33,7 @@ private CachedMessage(long messageId, long authorId,long channelId) { * Creates a {@link CachedMessage} with the given information. * @param messageId The Discord ID of the message * @param authorId The Discord ID of the message author + * @param channelId The Discord ID of the message channel * @param messageContent the textual content of the message * @param attachments The attachment URLs */ diff --git a/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java b/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java index d5acf1c1b..376abfba7 100644 --- a/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java +++ b/src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java @@ -2,7 +2,6 @@ import lombok.extern.slf4j.Slf4j; import net.discordjug.javabot.data.config.BotConfig; -import net.discordjug.javabot.data.config.guild.ModerationConfig; import net.discordjug.javabot.data.h2db.message_cache.MessageCache; import net.discordjug.javabot.data.h2db.message_cache.model.CachedMessage; import net.discordjug.javabot.systems.moderation.warn.model.WarnSeverity;