Skip to content

Commit 587f89b

Browse files
authored
Merge pull request #525 from danthe1st/spam-automod
improve spam automod and message cache, upgrade to JDK 25
2 parents 6a5b6d3 + e66ad87 commit 587f89b

File tree

10 files changed

+13109
-338
lines changed

10 files changed

+13109
-338
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ jobs:
1212
contents: read
1313
steps:
1414
- uses: actions/checkout@v4
15-
- name: Set up JDK 17
15+
- name: Set up JDK 25
1616
uses: actions/setup-java@v4
1717
with:
18-
java-version: '17'
18+
java-version: '25'
1919
distribution: 'temurin'
2020
- name: Grant execute permission for gradlew
2121
run: chmod +x gradlew
@@ -31,10 +31,10 @@ jobs:
3131
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }}
3232
steps:
3333
- uses: actions/checkout@v4
34-
- name: Set up JDK 21
34+
- name: Set up JDK 25
3535
uses: graalvm/setup-graalvm@v1
3636
with:
37-
java-version: '21'
37+
java-version: '25'
3838
distribution: 'graalvm-community'
3939
- name: Build native-image
4040
run: ./gradlew nativeCompile -Pprod

build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ plugins {
1111
}
1212

1313
java {
14-
sourceCompatibility = JavaVersion.VERSION_17
15-
targetCompatibility = JavaVersion.VERSION_17
14+
sourceCompatibility = JavaVersion.VERSION_25
15+
targetCompatibility = JavaVersion.VERSION_25
1616
}
1717

1818
group = "net.discordjug"
@@ -121,6 +121,7 @@ tasks.processTestAot {
121121
graalvmNative {
122122
binaries {
123123
named("main") {
124+
buildArgs.add("-H:+ForeignAPISupport")//needed for AWT/plotting, see https://bugs.openjdk.org/browse/JDK-8337237
124125
if (hasProperty("prod")) {
125126
buildArgs.add("-O3")
126127
} else {

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCache.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
import java.net.http.HttpResponse.BodyHandlers;
99
import java.nio.charset.StandardCharsets;
1010
import java.time.Instant;
11+
import java.time.OffsetDateTime;
1112
import java.time.ZoneOffset;
1213
import java.time.format.DateTimeFormatter;
14+
import java.util.ArrayDeque;
1315
import java.util.ArrayList;
16+
import java.util.Deque;
1417
import java.util.List;
1518
import java.util.concurrent.ExecutorService;
1619
import java.util.stream.Collectors;
@@ -35,6 +38,7 @@
3538
import net.dv8tion.jda.api.entities.Message.Attachment;
3639
import net.dv8tion.jda.api.entities.MessageEmbed;
3740
import net.dv8tion.jda.api.entities.User;
41+
import net.dv8tion.jda.api.entities.UserSnowflake;
3842
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
3943
import net.dv8tion.jda.api.interactions.components.buttons.Button;
4044
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
@@ -49,7 +53,7 @@ public class MessageCache {
4953
/**
5054
* A memory-cache (list) of sent Messages, wrapped to a {@link CachedMessage} object.
5155
*/
52-
public List<CachedMessage> cache = new ArrayList<>();
56+
public Deque<CachedMessage> cache = new ArrayDeque<>();
5357
/**
5458
* Amount of messages since the last synchronization.
5559
* <p>
@@ -74,12 +78,11 @@ public MessageCache(BotConfig botConfig, MessageCacheRepository cacheRepository,
7478
this.botConfig = botConfig;
7579
this.cacheRepository = cacheRepository;
7680
try {
77-
cache = cacheRepository.getAll();
81+
cache = new ArrayDeque<>(cacheRepository.getAll());
7882
} catch (DataAccessException e) {
7983
ExceptionLogger.capture(e, getClass().getSimpleName());
8084
log.error("Something went wrong during retrieval of stored messages.");
8185
}
82-
8386
}
8487

8588
/**
@@ -88,7 +91,7 @@ public MessageCache(BotConfig botConfig, MessageCacheRepository cacheRepository,
8891
public void synchronize() {
8992
asyncPool.execute(()->{
9093
cacheRepository.delete(cache.size());
91-
cacheRepository.insertList(cache);
94+
cacheRepository.insertList(new ArrayList<>(cache));
9295
messageCount = 0;
9396
log.info("Synchronized Database with local Cache.");
9497
});
@@ -151,6 +154,22 @@ public void sendDeletedMessageToLog(Guild guild, MessageChannel channel, CachedM
151154
requestMessageAttachments(message);
152155
});
153156
}
157+
158+
/**
159+
* Retrieves all cached messages that were sent after a passed timestamp.
160+
* @param timestamp the timestamp since when messages should be received
161+
* @return the messages sent after the given timestamp as a {@link List}
162+
*/
163+
public List<CachedMessage> getMessagesAfter(OffsetDateTime timestamp) {
164+
List<CachedMessage> cachedMessages = new ArrayList<>();
165+
for (CachedMessage msg : this.cache.reversed()) {
166+
if (UserSnowflake.fromId(msg.getMessageId()).getTimeCreated().isBefore(timestamp)) {
167+
return cachedMessages.reversed();
168+
}
169+
cachedMessages.add(msg);
170+
}
171+
return cachedMessages.reversed();
172+
}
154173

155174
/**
156175
* Requests each attachment from Discord's CDN.

src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCacheListener.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import lombok.RequiredArgsConstructor;
1515

16+
import java.util.Deque;
1617
import java.util.List;
1718
import java.util.Optional;
1819

@@ -33,16 +34,15 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) {
3334
@Override
3435
public void onMessageUpdate(@NotNull MessageUpdateEvent event) {
3536
if (this.ignoreMessageCache(event.getMessage())) return;
36-
List<CachedMessage> cache = messageCache.cache;
37+
Deque<CachedMessage> cache = messageCache.cache;
3738
Optional<CachedMessage> optional = cache.stream().filter(m -> m.getMessageId() == event.getMessageIdLong()).findFirst();
3839
CachedMessage before;
3940
if (optional.isPresent()) {
40-
before = optional.get();
41-
cache.set(cache.indexOf(before), CachedMessage.of(event.getMessage()));
41+
CachedMessage inCache= optional.get();
42+
before = new CachedMessage(inCache.getMessageId(), inCache.getAuthorId(), inCache.getMessageContent(), inCache.getAttachments());
43+
inCache.init(event.getMessage());
4244
} else {
43-
before = new CachedMessage();
44-
before.setMessageId(event.getMessageIdLong());
45-
before.setMessageContent("[unknown content]");
45+
before = new CachedMessage(event.getMessageIdLong(), event.getAuthor().getIdLong(), "[unknown content]", List.of());
4646
messageCache.cache(event.getMessage());
4747
}
4848
messageCache.sendUpdatedMessageToLog(event.getMessage(), before);

src/main/java/net/discordjug/javabot/data/h2db/message_cache/dao/MessageCacheRepository.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,15 @@ public boolean delete(int amount) throws DataAccessException {
110110
}
111111

112112
private CachedMessage read(ResultSet rs) throws SQLException {
113-
CachedMessage cachedMessage = new CachedMessage();
114-
cachedMessage.setMessageId(rs.getLong("message_cache.message_id"));
115-
cachedMessage.setAuthorId(rs.getLong("author_id"));
116-
cachedMessage.setMessageContent(rs.getString("message_content"));
113+
List<String> attachments = new ArrayList<>();
117114
String attachment = rs.getString("link");
118-
if(attachment!=null) {
119-
cachedMessage.getAttachments().add(attachment);
115+
if(attachment != null) {
116+
attachments.add(attachment);
120117
}
121-
return cachedMessage;
118+
return new CachedMessage(
119+
rs.getLong("message_cache.message_id"),
120+
rs.getLong("author_id"),
121+
rs.getString("message_content"),
122+
attachments);
122123
}
123124
}

src/main/java/net/discordjug/javabot/data/h2db/message_cache/model/CachedMessage.java

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,44 @@
33
import java.util.ArrayList;
44
import java.util.List;
55

6-
import lombok.Data;
6+
import lombok.EqualsAndHashCode;
7+
import lombok.Getter;
8+
import lombok.ToString;
79
import net.discordjug.javabot.util.MessageUtils;
810
import net.dv8tion.jda.api.entities.Message;
911
import net.dv8tion.jda.api.entities.Message.Attachment;
1012

1113
/**
1214
* Represents a cached Message.
1315
*/
14-
@Data
16+
@Getter
17+
@EqualsAndHashCode
18+
@ToString
1519
public class CachedMessage {
16-
private long messageId;
17-
private long authorId;
20+
private final long messageId;
21+
private final long authorId;
1822
private String messageContent;
1923
private List<String> attachments=new ArrayList<>();
24+
25+
private CachedMessage(long messageId, long authorId) {
26+
this.messageId = messageId;
27+
this.authorId = authorId;
28+
}
29+
30+
/**
31+
* Creates a {@link CachedMessage} with the given information.
32+
* @param messageId The Discord ID of the message
33+
* @param authorId The Discord ID of the message author
34+
* @param messageContent the textual content of the message
35+
* @param attachments The attachment URLs
36+
*/
37+
public CachedMessage(long messageId, long authorId, String messageContent, List<String> attachments) {
38+
super();
39+
this.messageId = messageId;
40+
this.authorId = authorId;
41+
this.messageContent = messageContent;
42+
this.attachments = List.copyOf(attachments);
43+
}
2044

2145
/**
2246
* Converts a {@link Message} object to a {@link CachedMessage}.
@@ -25,15 +49,25 @@ public class CachedMessage {
2549
* @return The built {@link CachedMessage}.
2650
*/
2751
public static CachedMessage of(Message message) {
28-
CachedMessage cachedMessage = new CachedMessage();
29-
cachedMessage.setMessageId(message.getIdLong());
30-
cachedMessage.setAuthorId(message.getAuthor().getIdLong());
31-
cachedMessage.setMessageContent(MessageUtils.getMessageContent(message).trim());
32-
cachedMessage.attachments = message
52+
CachedMessage cachedMessage = new CachedMessage(message.getIdLong(), message.getAuthor().getIdLong());
53+
cachedMessage.init(message);
54+
return cachedMessage;
55+
}
56+
57+
/**
58+
* Resets the current {@link CachedMessage} to have the content of the passed {@link Message}.
59+
* @param message the {@link Message} this object is set to.
60+
*/
61+
public void init(Message message) {
62+
messageContent = MessageUtils.getMessageContent(message).trim();
63+
this.attachments = message
3364
.getAttachments()
3465
.stream()
3566
.map(Attachment::getUrl)
3667
.toList();
37-
return cachedMessage;
68+
}
69+
70+
public void setMessageContent(String messageContent) {
71+
this.messageContent = messageContent;
3872
}
3973
}

src/main/java/net/discordjug/javabot/systems/moderation/AutoMod.java

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.extern.slf4j.Slf4j;
44
import net.discordjug.javabot.data.config.BotConfig;
5+
import net.discordjug.javabot.data.h2db.message_cache.MessageCache;
56
import net.discordjug.javabot.systems.moderation.warn.model.WarnSeverity;
67
import net.discordjug.javabot.systems.notification.NotificationService;
78
import net.discordjug.javabot.util.ExceptionLogger;
@@ -25,7 +26,6 @@
2526
import java.time.temporal.ChronoUnit;
2627
import java.util.Collections;
2728
import java.util.List;
28-
import java.util.Objects;
2929
import java.util.Scanner;
3030
import java.util.concurrent.TimeUnit;
3131
import java.util.regex.Matcher;
@@ -35,7 +35,6 @@
3535
* This class checks all incoming messages for potential spam/advertising and warns or mutes the potential offender.
3636
*/
3737
@Slf4j
38-
// TODO: Refactor this to be more efficient. Especially AutoMod#checkNewMessageAutomod
3938
public class AutoMod extends ListenerAdapter {
4039

4140
private static final Pattern INVITE_URL = Pattern.compile("discord(?:(\\.(?:me|io|gg)|sites\\.com)/.{0,4}|(?:app)?\\.com.{1,4}(?:invite|oauth2).{0,5}/)\\w+");
@@ -48,17 +47,20 @@ public class AutoMod extends ListenerAdapter {
4847
private final BotConfig botConfig;
4948
private List<String> spamUrls;
5049
private final ModerationService moderationService;
50+
private final MessageCache messageCache;
5151

5252
/**
5353
* Constructor of the class, that creates a list of strings with potential spam/scam urls.
5454
* @param notificationService The {@link QOTWPointsService}
5555
* @param botConfig The main configuration of the bot
5656
* @param moderationService Service object for moderating members
57+
* @param messageCache service for retrieving cached messages
5758
*/
58-
public AutoMod(NotificationService notificationService, BotConfig botConfig, ModerationService moderationService) {
59+
public AutoMod(NotificationService notificationService, BotConfig botConfig, ModerationService moderationService, MessageCache messageCache) {
5960
this.notificationService = notificationService;
6061
this.botConfig = botConfig;
6162
this.moderationService = moderationService;
63+
this.messageCache = messageCache;
6264
try(Scanner scan = new Scanner(new URL("https://raw.githubusercontent.com/DevSpen/scam-links/master/src/links.txt").openStream()).useDelimiter("\\A")) {
6365
String response = scan.next();
6466
spamUrls = List.of(response.split("\n"));
@@ -102,15 +104,20 @@ private boolean canBypassAutomod(Member member) {
102104
*/
103105
private void checkNewMessageAutomod(@Nonnull Message message) {
104106
// spam
105-
message.getChannel().getHistory().retrievePast(10).queue(messages -> {
106-
int spamCount = (int) messages.stream().filter(msg -> !msg.equals(message))
107-
// filter for spam
108-
.filter(msg -> msg.getAuthor().equals(message.getAuthor()) && !msg.getAuthor().isBot())
109-
.filter(msg -> (message.getTimeCreated().toEpochSecond() - msg.getTimeCreated().toEpochSecond()) < 6).count();
110-
if (spamCount > 5) {
111-
handleSpam(message, message.getMember());
112-
}
113-
});
107+
long spamCount = messageCache.getMessagesAfter(message.getTimeCreated().minusSeconds(6))
108+
.stream()
109+
.filter(cached -> cached.getMessageId() != message.getIdLong()) // exclude new/current message
110+
.filter(cached -> cached.getAuthorId() == message.getAuthor().getIdLong())
111+
.filter(cached ->
112+
// only java files -> not spam
113+
cached.getAttachments().isEmpty() ||
114+
cached.getAttachments().stream()
115+
.anyMatch(attachment -> !attachment.contains(".java?")))
116+
.count() + 1; // include new message
117+
118+
if (spamCount >= 5) {
119+
handleSpam(message, message.getMember());
120+
}
114121

115122
checkContentAutomod(message);
116123
}
@@ -149,16 +156,12 @@ private void doAutomodActions(Message message, String reason) {
149156
}
150157

151158
/**
152-
* Handles potential spam messages.
159+
* Handles detected spam messages.
153160
*
154-
* @param msg the message
161+
* @param msg the (last) spam message
155162
* @param member the member to be potentially warned
156163
*/
157164
private void handleSpam(@Nonnull Message msg, Member member) {
158-
// java files -> not spam
159-
if (!msg.getAttachments().isEmpty() && msg.getAttachments().stream().allMatch(a -> Objects.equals(a.getFileExtension(), "java"))) {
160-
return;
161-
}
162165
moderationService
163166
.timeout(
164167
member.getUser(),
@@ -168,6 +171,7 @@ private void handleSpam(@Nonnull Message msg, Member member) {
168171
msg.getChannel(),
169172
false
170173
);
174+
msg.delete().queue();
171175
}
172176

173177
/**

0 commit comments

Comments
 (0)