Skip to content

Commit 37637db

Browse files
authored
Merge pull request #555 from danthe1st/custom-automod-search
add search-based condition to rule-based automod
2 parents 957ab6d + 75886a1 commit 37637db

8 files changed

Lines changed: 102 additions & 53 deletions

File tree

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<parent>
88
<groupId>org.springframework.boot</groupId>
99
<artifactId>spring-boot-starter-parent</artifactId>
10-
<version>4.0.3</version>
10+
<version>4.1.0</version>
1111
</parent>
1212

1313
<properties>
@@ -43,7 +43,7 @@
4343
<dependency>
4444
<groupId>net.dv8tion</groupId>
4545
<artifactId>JDA</artifactId>
46-
<version>6.2.0</version>
46+
<version>6.5.0</version>
4747
<exclusions>
4848
<exclusion>
4949
<artifactId>opus-java</artifactId>

src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package net.discordjug.javabot;
22

3+
import java.util.concurrent.ThreadPoolExecutor;
4+
35
import club.minnced.discord.webhook.send.WebhookEmbed;
46
import com.zaxxer.hikari.HikariConfig;
57
import net.discordjug.javabot.data.config.BotConfig;
@@ -15,6 +17,7 @@
1517
import net.discordjug.javabot.data.config.guild.QOTWConfig;
1618
import net.discordjug.javabot.data.config.guild.ServerLockConfig;
1719
import net.discordjug.javabot.data.config.guild.StarboardConfig;
20+
import net.dv8tion.jda.api.entities.SoundboardSound;
1821
import net.dv8tion.jda.api.hooks.ListenerAdapter;
1922
import net.dv8tion.jda.internal.entities.GuildVoiceStateImpl;
2023
import net.dv8tion.jda.internal.requests.restaction.PermOverrideData;
@@ -60,6 +63,8 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
6063
// JDA needs to be able to access listener methods
6164
hints.reflection().registerType(ListenerAdapter.class, MemberCategory.INVOKE_PUBLIC_METHODS);
6265

66+
hints.reflection().registerType(ThreadPoolExecutor.class, MemberCategory.INVOKE_PUBLIC_METHODS);
67+
6368
// caffeine
6469
hints.reflection().registerTypeIfPresent(getClass().getClassLoader(), "com.github.benmanes.caffeine.cache.SSW", MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
6570

@@ -77,5 +82,6 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
7782
}
7883

7984
hints.reflection().registerType(GuildVoiceStateImpl[].class, MemberCategory.UNSAFE_ALLOCATED);
85+
hints.reflection().registerType(SoundboardSound[].class, MemberCategory.UNSAFE_ALLOCATED);
8086
}
8187
}

src/main/java/net/discordjug/javabot/data/config/guild/MessageRule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ public class MessageRule {
3333
*/
3434
private Set<String> attachmentSHAs = new HashSet<>();
3535

36+
/**
37+
* There must be no messages older than that number of seconds.
38+
* If this value is {@code 0} or negative, this condition is ignored.
39+
*/
40+
private long noMessagesFromAuthorBeforeSeconds = -1;
41+
3642
/**
3743
* The action to execute on the message.
3844
*/

src/main/java/net/discordjug/javabot/data/h2db/commands/MessageCacheInfoSubcommand.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
44
import net.discordjug.javabot.data.config.BotConfig;
55
import net.discordjug.javabot.data.config.GuildConfig;
6+
import net.discordjug.javabot.data.config.guild.MessageCacheConfig;
67
import net.discordjug.javabot.data.h2db.DbActions;
78
import net.discordjug.javabot.data.h2db.message_cache.MessageCache;
89
import net.discordjug.javabot.util.Responses;
@@ -44,13 +45,14 @@ public void execute(SlashCommandInteractionEvent event) {
4445

4546
private MessageEmbed buildInfoEmbed(GuildConfig config, User author) {
4647
long messages = dbActions.count("SELECT count(*) FROM message_cache");
47-
int maxMessages = config.getMessageCacheConfig().getMaxCachedMessages();
48+
MessageCacheConfig messageCacheConfig = config.getMessageCacheConfig();
49+
int maxMessages = messageCacheConfig.getMaxCachedMessages();
4850
return new EmbedBuilder()
4951
.setAuthor(UserUtils.getUserTag(author), null, author.getEffectiveAvatarUrl())
5052
.setTitle("Message Cache Info")
5153
.setColor(Responses.Type.DEFAULT.getColor())
5254
.addField("Table Size", dbActions.getLogicalSize("message_cache") + " bytes", false)
53-
.addField("Message Count", String.valueOf(messageCache.getMessageCount()), true)
55+
.addField("Messages since synchronization", messageCache.getMessageCount() + "/" + messageCacheConfig.getMessageSynchronizationInterval(), true)
5456
.addField("Cached (Memory)", String.format("%s/%s (%.2f%%)", messageCache.cache.size(), maxMessages, ((float) messageCache.cache.size() / maxMessages) * 100), true)
5557
.addField("Cached (Database)", String.format("%s/%s (%.2f%%)", messages, maxMessages, ((float) messages / maxMessages) * 100), true)
5658
.build();

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.ArrayList;
3737
import java.util.Deque;
3838
import java.util.List;
39+
import java.util.concurrent.ConcurrentLinkedDeque;
3940
import java.util.concurrent.ExecutorService;
4041
import java.util.stream.Collectors;
4142

@@ -51,7 +52,7 @@ public class MessageCache {
5152
/**
5253
* A memory-cache (list) of sent Messages, wrapped to a {@link CachedMessage} object.
5354
*/
54-
public Deque<CachedMessage> cache = new ArrayDeque<>();
55+
public Deque<CachedMessage> cache = new ConcurrentLinkedDeque<>();
5556
/**
5657
* Amount of messages since the last synchronization.
5758
* <p>
@@ -96,8 +97,10 @@ public void synchronize() {
9697
* Synchronizes Messages saved in the Database with what is currently stored in memory and wait until the synchronization finishes.
9798
*/
9899
public void synchronizeNow() {
99-
cacheRepository.delete(cache.size());
100-
cacheRepository.insertList(new ArrayList<>(cache));
100+
if (messageCount == 0) {
101+
return;
102+
}
103+
cacheRepository.replaceWithList(new ArrayList<>(cache));
101104
messageCount = 0;
102105
log.info("Synchronized Database with local Cache.");
103106
}

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
99
import org.springframework.jdbc.core.JdbcTemplate;
1010
import org.springframework.stereotype.Repository;
11+
import org.springframework.transaction.support.TransactionTemplate;
1112

1213
import java.sql.*;
1314
import java.util.ArrayList;
@@ -23,12 +24,31 @@
2324
@Repository
2425
public class MessageCacheRepository {
2526
private final JdbcTemplate jdbcTemplate;
27+
private final TransactionTemplate template;
28+
29+
/**
30+
* Replaces the entire message cache with a {@link List} of {@link CachedMessage} objects.
31+
*
32+
* @param messages The new message cache.
33+
*/
34+
public void replaceWithList(@NotNull List<CachedMessage> messages) {
35+
template.execute(_ -> {
36+
clear();
37+
insertList(messages);
38+
return null;
39+
});
40+
}
41+
42+
private void clear() throws DataAccessException {
43+
jdbcTemplate.update("DELETE FROM message_cache_attachments WHERE 1 = 1");
44+
jdbcTemplate.update("DELETE FROM message_cache WHERE 1 = 1");
45+
}
2646

2747
/**
2848
* Inserts a {@link List} of {@link CachedMessage} objects.
2949
*
3050
* @param messages The List to insert.
31-
* @throws SQLException If an error occurs.
51+
* @throws DataAccessException If an error occurs.
3252
*/
3353
public void insertList(@NotNull List<CachedMessage> messages) throws DataAccessException {
3454
jdbcTemplate.batchUpdate("MERGE INTO message_cache (message_id, author_id, channel_id, message_content) VALUES (?, ?, ?, ?)",
@@ -78,11 +98,10 @@ public int getBatchSize() {
7898
* Gets all Messages from the Database.
7999
*
80100
* @return A {@link List} of {@link CachedMessage}s.
81-
* @throws SQLException If anything goes wrong.
82101
*/
83102
public List<CachedMessage> getAll() throws DataAccessException {
84103
List<CachedMessage> messagesWithLink = jdbcTemplate.query(
85-
"SELECT * FROM message_cache LEFT JOIN message_cache_attachments ON message_cache.message_id = message_cache_attachments.message_id",
104+
"SELECT * FROM message_cache LEFT JOIN message_cache_attachments ON message_cache.message_id = message_cache_attachments.message_id ORDER BY message_cache.message_id ASC",
86105
(rs, _) -> this.read(rs));
87106
Map<Long, CachedMessage> messages=new LinkedHashMap<>();
88107
for (CachedMessage msg : messagesWithLink) {
@@ -95,22 +114,6 @@ public List<CachedMessage> getAll() throws DataAccessException {
95114
return new ArrayList<>(messages.values());
96115
}
97116

98-
/**
99-
* Deletes the given amount of Messages.
100-
*
101-
* @param amount The amount to delete.
102-
* @return If any rows we're affected.
103-
* @throws SQLException If anything goes wrong.
104-
*/
105-
public boolean delete(int amount) throws DataAccessException {
106-
int rows = jdbcTemplate.update("DELETE FROM message_cache LIMIT ?", amount);
107-
if(rows > 0){
108-
jdbcTemplate.update("DELETE FROM message_cache_attachments WHERE message_id NOT IN (SELECT message_id FROM message_cache)");
109-
return true;
110-
}
111-
return false;
112-
}
113-
114117
private CachedMessage read(ResultSet rs) throws SQLException {
115118
List<String> attachments = new ArrayList<>();
116119
String attachment = rs.getString("link");

src/main/java/net/discordjug/javabot/listener/filter/MessageFilterHandler.java

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

1414
import java.util.ArrayList;
1515
import java.util.List;
16+
import java.util.concurrent.Executor;
1617

1718
/**
1819
* This class is responsible for calling {@link MessageFilter}s on incoming messages and optionally replacing the message.
@@ -28,6 +29,7 @@ public class MessageFilterHandler extends ListenerAdapter {
2829

2930
private final List<MessageFilter> filters;
3031
private final AutoMod autoMod;
32+
private final Executor asyncPool;
3133

3234
@Override
3335
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
@@ -42,36 +44,39 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) {
4244
new ArrayList<>(event.getMessage().getEmbeds())
4345
);
4446

45-
boolean handled = false;
4647

47-
for (MessageFilter filter : filters) {
48-
MessageModificationStatus status = filter.processMessage(content);
49-
if (status == MessageModificationStatus.MODIFIED) {
50-
handled = true;
51-
} else if (status == MessageModificationStatus.STOP_PROCESSING) {
52-
return;
53-
}
54-
}
48+
asyncPool.execute(() -> {
49+
boolean handled = false;
5550

56-
if (handled) {
57-
IWebhookContainer webhookContainer = null;
58-
long threadId = 0;
59-
if (event.isFromType(ChannelType.TEXT)) {
60-
webhookContainer = event.getChannel().asTextChannel();
51+
for (MessageFilter filter : filters) {
52+
MessageModificationStatus status = filter.processMessage(content);
53+
if (status == MessageModificationStatus.MODIFIED) {
54+
handled = true;
55+
} else if (status == MessageModificationStatus.STOP_PROCESSING) {
56+
return;
57+
}
6158
}
62-
if (event.isFromThread()) {
63-
StandardGuildChannel parentChannel = event.getChannel()
64-
.asThreadChannel()
65-
.getParentChannel()
66-
.asStandardGuildChannel();
67-
threadId = event.getChannel().getIdLong();
68-
webhookContainer = (IWebhookContainer) parentChannel;
59+
60+
if (handled) {
61+
IWebhookContainer webhookContainer = null;
62+
long threadId = 0;
63+
if (event.isFromType(ChannelType.TEXT)) {
64+
webhookContainer = event.getChannel().asTextChannel();
65+
}
66+
if (event.isFromThread()) {
67+
StandardGuildChannel parentChannel = event.getChannel()
68+
.asThreadChannel()
69+
.getParentChannel()
70+
.asStandardGuildChannel();
71+
threadId = event.getChannel().getIdLong();
72+
webhookContainer = (IWebhookContainer) parentChannel;
73+
}
74+
if (webhookContainer == null) {
75+
return;
76+
}
77+
replaceMessage(webhookContainer, threadId, content);
6978
}
70-
if (webhookContainer == null) {
71-
return;
72-
}
73-
replaceMessage(webhookContainer, threadId, content);
74-
}
79+
});
7580
}
7681

7782
private boolean shouldRunFilters(@NotNull MessageReceivedEvent event) {

src/main/java/net/discordjug/javabot/listener/filter/MessageRuleFilter.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.security.MessageDigest;
1010
import java.security.NoSuchAlgorithmException;
1111
import java.time.Duration;
12+
import java.time.OffsetDateTime;
1213
import java.util.Base64;
1314
import java.util.List;
1415
import java.util.stream.Collectors;
@@ -25,6 +26,8 @@
2526
import net.discordjug.javabot.util.GsonUtils;
2627
import net.dv8tion.jda.api.EmbedBuilder;
2728
import net.dv8tion.jda.api.entities.Message.Attachment;
29+
import net.dv8tion.jda.api.utils.TimeUtil;
30+
import net.dv8tion.jda.api.entities.Guild;
2831
import net.dv8tion.jda.api.entities.Message;
2932
import org.springframework.stereotype.Component;
3033

@@ -103,7 +106,28 @@ private boolean matches(MessageContent content, MessageRule rule) {
103106
}
104107
}
105108
}
106-
return matchesSHA;
109+
if (!matchesSHA) {
110+
return false;
111+
}
112+
if (rule.getNoMessagesFromAuthorBeforeSeconds() > 0) {
113+
OffsetDateTime maxTime = content.event().getMessage().getTimeCreated()
114+
.minusSeconds(rule.getNoMessagesFromAuthorBeforeSeconds());
115+
long authorId = content.event().getAuthor().getIdLong();
116+
return !hasMessagesOlderThan(maxTime, content.event().getGuild(), authorId);
117+
}
118+
return true;
119+
}
120+
121+
private boolean hasMessagesOlderThan(OffsetDateTime maxTime, Guild guild, long authorId) {
122+
if (messageCache.cache.stream()
123+
.filter(msg -> TimeUtil.getTimeCreated(msg.getMessageId()).isBefore(maxTime))
124+
.anyMatch(msg -> msg.getAuthorId() == authorId)) {
125+
return true;
126+
}
127+
return guild.searchMessages()
128+
.authors(authorId)
129+
.maxId(TimeUtil.getDiscordTimestamp(maxTime.toInstant().toEpochMilli()))
130+
.complete().asResults().getTotalResults() > 0;
107131
}
108132

109133
private String computeAttachmentDescription(List<Message.Attachment> attachments) {

0 commit comments

Comments
 (0)