diff --git a/src/main/java/net/javadiscord/javabot2/Bot.java b/src/main/java/net/javadiscord/javabot2/Bot.java index fe03a32..8545421 100644 --- a/src/main/java/net/javadiscord/javabot2/Bot.java +++ b/src/main/java/net/javadiscord/javabot2/Bot.java @@ -9,11 +9,17 @@ import com.zaxxer.hikari.HikariDataSource; import net.javadiscord.javabot2.command.SlashCommandListener; import net.javadiscord.javabot2.config.BotConfig; +import net.javadiscord.javabot2.systems.moderation.SpamListener; +import net.javadiscord.javabot2.systems.moderation.MessageCache; import org.javacord.api.DiscordApi; import org.javacord.api.DiscordApiBuilder; import org.javacord.api.entity.intent.Intent; +import org.javacord.api.entity.message.Message; +import org.javacord.api.entity.message.MessageAuthor; import java.nio.file.Path; +import java.util.LinkedList; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -46,6 +52,11 @@ public class Bot { */ public static ScheduledExecutorService asyncPool; + /** + * The message cache. + */ + public static ConcurrentHashMap> messageCache; + // Hide constructor. private Bot() {} @@ -60,8 +71,12 @@ public static void main(String[] args) { .setToken(config.getSystems().getDiscordBotToken()) .setAllIntentsExcept(Intent.GUILD_MESSAGE_TYPING, Intent.GUILD_PRESENCES, Intent.GUILD_VOICE_STATES) .login().join(); + + initListeners(api); + config.loadGuilds(api.getServers()); // Once we've logged in, load all guild config files. config.flush(); // Flush to save any new config files that are generated for new guilds. + SlashCommandListener commandListener = new SlashCommandListener( api, args.length > 0 && args[0].equalsIgnoreCase("--register-commands"), @@ -70,6 +85,18 @@ public static void main(String[] args) { api.addSlashCommandCreateListener(commandListener); } + /** + * Initializes and adds all listeners to the API. + * @param api the API + */ + private static void initListeners(DiscordApi api) { + MessageCache cache = new MessageCache(); + messageCache = cache.getCache(); + + api.addMessageCreateListener(new SpamListener()); + api.addMessageCreateListener(cache); + } + /** * Initializes all the basic data sources that are needed by the bot's other * capabilities. This should be called before logging in diff --git a/src/main/java/net/javadiscord/javabot2/config/guild/ModerationConfig.java b/src/main/java/net/javadiscord/javabot2/config/guild/ModerationConfig.java index 4466891..bfca37f 100644 --- a/src/main/java/net/javadiscord/javabot2/config/guild/ModerationConfig.java +++ b/src/main/java/net/javadiscord/javabot2/config/guild/ModerationConfig.java @@ -16,6 +16,31 @@ public class ModerationConfig extends GuildConfigItem { */ private long staffRoleId; + /** + * The amount of seconds to be looked into the past to determine if a user is spamming. + */ + private int pastMessageCountBeforeDurationInSeconds; + + /** + * The amount of messages to be sent within {@link #pastMessageCountBeforeDurationInSeconds}. + */ + private int messageSpamAmount; + + /** + * The amount of messages to be cached for each user. + */ + private int cachedMessagesPerUser; + + /** + * The frequency of cleaning up the cached messages. Amount in minutes. + */ + private int cachedMessageCleanupFrequency; + + /** + * The amount of minutes for removing this cached messages. + */ + private int amountOfMinutesForRemoval; + public Role getStaffRole() { return this.getGuild().getRoleById(staffRoleId).orElseThrow(); } diff --git a/src/main/java/net/javadiscord/javabot2/systems/moderation/MessageCache.java b/src/main/java/net/javadiscord/javabot2/systems/moderation/MessageCache.java new file mode 100644 index 0000000..0210454 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/moderation/MessageCache.java @@ -0,0 +1,82 @@ +package net.javadiscord.javabot2.systems.moderation; + +import net.javadiscord.javabot2.Bot; +import org.javacord.api.entity.message.Message; +import org.javacord.api.entity.message.MessageAuthor; +import org.javacord.api.event.message.MessageCreateEvent; +import org.javacord.api.listener.message.MessageCreateListener; + +import java.time.Duration; +import java.time.Instant; +import java.util.LinkedList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * Caches incoming messages and cleans it out when needed. + */ +public class MessageCache implements MessageCreateListener { + + private final ConcurrentHashMap lastModifications; + private final ConcurrentHashMap> cache; + + /** + * Creates the cache. + */ + public MessageCache() { + lastModifications = new ConcurrentHashMap<>(); + cache = new ConcurrentHashMap<>(); + // TODO config + Bot.asyncPool.scheduleAtFixedRate(this::clean, -1, -1, TimeUnit.MINUTES); + } + + @Override + public void onMessageCreate(MessageCreateEvent event) { + if (!event.getMessageAuthor().isYourself()) { + add(event.getMessage()); + } + } + + /** + * Removes all Map entries which have not been modified within the last 10 minutes. + */ + private void clean() { + lastModifications.forEach((messageAuthor, instant) -> { + // if last modification is older than n minutes + // TODO config + if (instant.isAfter(Instant.now().minus(Duration.ofMinutes(-1)))) { + cache.remove(messageAuthor); + lastModifications.remove(messageAuthor); + } + }); + } + + /** + * Add the message either as a new Map entry or to the existing list per user. + * Also removes if the amount of messages per user is over a certain treshold. + * @param msg the message to be added + */ + private void add(Message msg) { + if (cache.containsKey(msg.getAuthor())) { + LinkedList msgList = cache.get(msg.getAuthor()); + msgList.offer(msg); + lastModifications.replace(msg.getAuthor(), Instant.now()); + + // TODO config + if (msgList.size() >= -1) { + msgList.poll(); + } + + } else { + LinkedList messages = new LinkedList<>(); + messages.add(msg); + cache.put(msg.getAuthor(), messages); + lastModifications.put(msg.getAuthor(), Instant.now()); + } + } + + public ConcurrentHashMap> getCache() { + return cache; + } +} + diff --git a/src/main/java/net/javadiscord/javabot2/systems/moderation/SpamListener.java b/src/main/java/net/javadiscord/javabot2/systems/moderation/SpamListener.java new file mode 100644 index 0000000..0015c48 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/systems/moderation/SpamListener.java @@ -0,0 +1,34 @@ +package net.javadiscord.javabot2.systems.moderation; + +import net.javadiscord.javabot2.Bot; +import org.javacord.api.event.message.MessageCreateEvent; +import org.javacord.api.listener.message.MessageCreateListener; + +import java.time.Duration; +import java.time.Instant; + +/** + * Listens for spam using the {@link MessageCache}. + */ +public class SpamListener implements MessageCreateListener { + + @Override + public void onMessageCreate(MessageCreateEvent event) { + if (!event.getMessageAuthor().isYourself() && Bot.messageCache.containsKey(event.getMessageAuthor())) { + int amountOfMessages = (int) Bot.messageCache.get(event.getMessageAuthor()) + .stream() + //TODO get time from config + .filter(msg -> msg.getCreationTimestamp().isAfter(Instant.now().minus(Duration.ofSeconds(-1)))) + .count(); + + //TODO get amount from config + if (amountOfMessages >= -1) { + //TODO spam detected + // placeholder for checkstyle not to complain. + // should be replaced with a warn + purge which is not implemented yet + event.deleteMessage("spam"); + } + } + } + +}