diff --git a/pom.xml b/pom.xml index cf7167434..9a881c6c6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.3 + 4.1.0 @@ -43,7 +43,7 @@ net.dv8tion JDA - 6.2.0 + 6.5.0 opus-java diff --git a/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java b/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java index 750530da1..1f30b4136 100644 --- a/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java +++ b/src/main/java/net/discordjug/javabot/RuntimeHintsConfiguration.java @@ -1,5 +1,7 @@ package net.discordjug.javabot; +import java.util.concurrent.ThreadPoolExecutor; + import club.minnced.discord.webhook.send.WebhookEmbed; import com.zaxxer.hikari.HikariConfig; import net.discordjug.javabot.data.config.BotConfig; @@ -15,6 +17,7 @@ import net.discordjug.javabot.data.config.guild.QOTWConfig; import net.discordjug.javabot.data.config.guild.ServerLockConfig; import net.discordjug.javabot.data.config.guild.StarboardConfig; +import net.dv8tion.jda.api.entities.SoundboardSound; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.internal.entities.GuildVoiceStateImpl; import net.dv8tion.jda.internal.requests.restaction.PermOverrideData; @@ -60,6 +63,8 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // JDA needs to be able to access listener methods hints.reflection().registerType(ListenerAdapter.class, MemberCategory.INVOKE_PUBLIC_METHODS); + hints.reflection().registerType(ThreadPoolExecutor.class, MemberCategory.INVOKE_PUBLIC_METHODS); + // caffeine hints.reflection().registerTypeIfPresent(getClass().getClassLoader(), "com.github.benmanes.caffeine.cache.SSW", MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); @@ -77,5 +82,6 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { } hints.reflection().registerType(GuildVoiceStateImpl[].class, MemberCategory.UNSAFE_ALLOCATED); + hints.reflection().registerType(SoundboardSound[].class, MemberCategory.UNSAFE_ALLOCATED); } } diff --git a/src/main/java/net/discordjug/javabot/data/config/guild/MessageRule.java b/src/main/java/net/discordjug/javabot/data/config/guild/MessageRule.java index 68f742d21..5b6537d88 100644 --- a/src/main/java/net/discordjug/javabot/data/config/guild/MessageRule.java +++ b/src/main/java/net/discordjug/javabot/data/config/guild/MessageRule.java @@ -33,6 +33,12 @@ public class MessageRule { */ private Set attachmentSHAs = new HashSet<>(); + /** + * There must be no messages older than that number of seconds. + * If this value is {@code 0} or negative, this condition is ignored. + */ + private long noMessagesFromAuthorBeforeSeconds = -1; + /** * The action to execute on the message. */ diff --git a/src/main/java/net/discordjug/javabot/data/h2db/commands/MessageCacheInfoSubcommand.java b/src/main/java/net/discordjug/javabot/data/h2db/commands/MessageCacheInfoSubcommand.java index 5086883a2..4f0f46192 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/commands/MessageCacheInfoSubcommand.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/commands/MessageCacheInfoSubcommand.java @@ -3,6 +3,7 @@ import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; import net.discordjug.javabot.data.config.BotConfig; import net.discordjug.javabot.data.config.GuildConfig; +import net.discordjug.javabot.data.config.guild.MessageCacheConfig; import net.discordjug.javabot.data.h2db.DbActions; import net.discordjug.javabot.data.h2db.message_cache.MessageCache; import net.discordjug.javabot.util.Responses; @@ -44,13 +45,14 @@ public void execute(SlashCommandInteractionEvent event) { private MessageEmbed buildInfoEmbed(GuildConfig config, User author) { long messages = dbActions.count("SELECT count(*) FROM message_cache"); - int maxMessages = config.getMessageCacheConfig().getMaxCachedMessages(); + MessageCacheConfig messageCacheConfig = config.getMessageCacheConfig(); + int maxMessages = messageCacheConfig.getMaxCachedMessages(); return new EmbedBuilder() .setAuthor(UserUtils.getUserTag(author), null, author.getEffectiveAvatarUrl()) .setTitle("Message Cache Info") .setColor(Responses.Type.DEFAULT.getColor()) .addField("Table Size", dbActions.getLogicalSize("message_cache") + " bytes", false) - .addField("Message Count", String.valueOf(messageCache.getMessageCount()), true) + .addField("Messages since synchronization", messageCache.getMessageCount() + "/" + messageCacheConfig.getMessageSynchronizationInterval(), true) .addField("Cached (Memory)", String.format("%s/%s (%.2f%%)", messageCache.cache.size(), maxMessages, ((float) messageCache.cache.size() / maxMessages) * 100), true) .addField("Cached (Database)", String.format("%s/%s (%.2f%%)", messages, maxMessages, ((float) messages / maxMessages) * 100), true) .build(); diff --git a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCache.java b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCache.java index e729b7a1e..0c95d3abb 100644 --- a/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCache.java +++ b/src/main/java/net/discordjug/javabot/data/h2db/message_cache/MessageCache.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Deque; import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; @@ -50,7 +51,7 @@ public class MessageCache { /** * A memory-cache (list) of sent Messages, wrapped to a {@link CachedMessage} object. */ - public Deque cache = new ArrayDeque<>(); + public Deque cache = new ConcurrentLinkedDeque<>(); /** * Amount of messages since the last synchronization. *

@@ -95,8 +96,10 @@ public void synchronize() { * Synchronizes Messages saved in the Database with what is currently stored in memory and wait until the synchronization finishes. */ public void synchronizeNow() { - cacheRepository.delete(cache.size()); - cacheRepository.insertList(new ArrayList<>(cache)); + if (messageCount == 0) { + return; + } + cacheRepository.replaceWithList(new ArrayList<>(cache)); messageCount = 0; log.info("Synchronized Database with local Cache."); } 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..8af5c7bd3 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 @@ -8,6 +8,7 @@ import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import org.springframework.transaction.support.TransactionTemplate; import java.sql.*; import java.util.ArrayList; @@ -23,14 +24,27 @@ @Repository public class MessageCacheRepository { private final JdbcTemplate jdbcTemplate; - + private final TransactionTemplate template; + /** - * Inserts a {@link List} of {@link CachedMessage} objects. + * Replaces the entire message cache with a {@link List} of {@link CachedMessage} objects. * - * @param messages The List to insert. - * @throws SQLException If an error occurs. + * @param messages The new message cache. */ - public void insertList(@NotNull List messages) throws DataAccessException { + public void replaceWithList(@NotNull List messages) { + template.execute(_ -> { + clear(); + insertList(messages); + return null; + }); + } + + private void clear() throws DataAccessException { + jdbcTemplate.update("DELETE FROM message_cache_attachments WHERE 1 = 1"); + jdbcTemplate.update("DELETE FROM message_cache WHERE 1 = 1"); + } + + private void insertList(@NotNull List messages) throws DataAccessException { jdbcTemplate.batchUpdate("MERGE INTO message_cache (message_id, author_id, message_content) VALUES (?, ?, ?)", new BatchPreparedStatementSetter() { @Override @@ -77,11 +91,10 @@ public int getBatchSize() { * Gets all Messages from the Database. * * @return A {@link List} of {@link CachedMessage}s. - * @throws SQLException If anything goes wrong. */ public List getAll() throws DataAccessException { List messagesWithLink = jdbcTemplate.query( - "SELECT * FROM message_cache LEFT JOIN message_cache_attachments ON message_cache.message_id = message_cache_attachments.message_id", + "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", (rs, _) -> this.read(rs)); Map messages=new LinkedHashMap<>(); for (CachedMessage msg : messagesWithLink) { @@ -94,22 +107,6 @@ public List getAll() throws DataAccessException { return new ArrayList<>(messages.values()); } - /** - * Deletes the given amount of Messages. - * - * @param amount The amount to delete. - * @return If any rows we're affected. - * @throws SQLException If anything goes wrong. - */ - public boolean delete(int amount) throws DataAccessException { - int rows = jdbcTemplate.update("DELETE FROM message_cache LIMIT ?", amount); - if(rows > 0){ - jdbcTemplate.update("DELETE FROM message_cache_attachments WHERE message_id NOT IN (SELECT message_id FROM message_cache)"); - return true; - } - return false; - } - private CachedMessage read(ResultSet rs) throws SQLException { List attachments = new ArrayList<>(); String attachment = rs.getString("link"); diff --git a/src/main/java/net/discordjug/javabot/listener/filter/MessageFilterHandler.java b/src/main/java/net/discordjug/javabot/listener/filter/MessageFilterHandler.java index 94205eb37..528ab7fd6 100644 --- a/src/main/java/net/discordjug/javabot/listener/filter/MessageFilterHandler.java +++ b/src/main/java/net/discordjug/javabot/listener/filter/MessageFilterHandler.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; /** * 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 { private final List filters; private final AutoMod autoMod; + private final Executor asyncPool; @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { @@ -42,36 +44,39 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) { new ArrayList<>(event.getMessage().getEmbeds()) ); - boolean handled = false; - for (MessageFilter filter : filters) { - MessageModificationStatus status = filter.processMessage(content); - if (status == MessageModificationStatus.MODIFIED) { - handled = true; - } else if (status == MessageModificationStatus.STOP_PROCESSING) { - return; - } - } + asyncPool.execute(() -> { + boolean handled = false; - if (handled) { - IWebhookContainer webhookContainer = null; - long threadId = 0; - if (event.isFromType(ChannelType.TEXT)) { - webhookContainer = event.getChannel().asTextChannel(); + for (MessageFilter filter : filters) { + MessageModificationStatus status = filter.processMessage(content); + if (status == MessageModificationStatus.MODIFIED) { + handled = true; + } else if (status == MessageModificationStatus.STOP_PROCESSING) { + return; + } } - if (event.isFromThread()) { - StandardGuildChannel parentChannel = event.getChannel() - .asThreadChannel() - .getParentChannel() - .asStandardGuildChannel(); - threadId = event.getChannel().getIdLong(); - webhookContainer = (IWebhookContainer) parentChannel; + + if (handled) { + IWebhookContainer webhookContainer = null; + long threadId = 0; + if (event.isFromType(ChannelType.TEXT)) { + webhookContainer = event.getChannel().asTextChannel(); + } + if (event.isFromThread()) { + StandardGuildChannel parentChannel = event.getChannel() + .asThreadChannel() + .getParentChannel() + .asStandardGuildChannel(); + threadId = event.getChannel().getIdLong(); + webhookContainer = (IWebhookContainer) parentChannel; + } + if (webhookContainer == null) { + return; + } + replaceMessage(webhookContainer, threadId, content); } - if (webhookContainer == null) { - return; - } - replaceMessage(webhookContainer, threadId, content); - } + }); } private boolean shouldRunFilters(@NotNull MessageReceivedEvent event) { diff --git a/src/main/java/net/discordjug/javabot/listener/filter/MessageRuleFilter.java b/src/main/java/net/discordjug/javabot/listener/filter/MessageRuleFilter.java index 4ae9a6c7e..beda91832 100644 --- a/src/main/java/net/discordjug/javabot/listener/filter/MessageRuleFilter.java +++ b/src/main/java/net/discordjug/javabot/listener/filter/MessageRuleFilter.java @@ -9,6 +9,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; +import java.time.OffsetDateTime; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -25,6 +26,8 @@ import net.discordjug.javabot.util.GsonUtils; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message.Attachment; +import net.dv8tion.jda.api.utils.TimeUtil; +import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import org.springframework.stereotype.Component; @@ -103,7 +106,28 @@ private boolean matches(MessageContent content, MessageRule rule) { } } } - return matchesSHA; + if (!matchesSHA) { + return false; + } + if (rule.getNoMessagesFromAuthorBeforeSeconds() > 0) { + OffsetDateTime maxTime = content.event().getMessage().getTimeCreated() + .minusSeconds(rule.getNoMessagesFromAuthorBeforeSeconds()); + long authorId = content.event().getAuthor().getIdLong(); + return !hasMessagesOlderThan(maxTime, content.event().getGuild(), authorId); + } + return true; + } + + private boolean hasMessagesOlderThan(OffsetDateTime maxTime, Guild guild, long authorId) { + if (messageCache.cache.stream() + .filter(msg -> TimeUtil.getTimeCreated(msg.getMessageId()).isBefore(maxTime)) + .anyMatch(msg -> msg.getAuthorId() == authorId)) { + return true; + } + return guild.searchMessages() + .authors(authorId) + .maxId(TimeUtil.getDiscordTimestamp(maxTime.toInstant().toEpochMilli())) + .complete().asResults().getTotalResults() > 0; } private String computeAttachmentDescription(List attachments) {