Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

Spam detection #14

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/main/java/net/javadiscord/javabot2/Bot.java
Original file line number Diff line number Diff line change
@@ -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<MessageAuthor, LinkedList<Message>> messageCache;
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

// 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);

cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
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();
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

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 <strong>before</strong> logging in
Original file line number Diff line number Diff line change
@@ -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;
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

/**
* The amount of messages to be sent within {@link #pastMessageCountBeforeDurationInSeconds}.
*/
private int messageSpamAmount;
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;

cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
public Role getStaffRole() {
return this.getGuild().getRoleById(staffRoleId).orElseThrow();
}
Original file line number Diff line number Diff line change
@@ -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<MessageAuthor, Instant> lastModifications;
private final ConcurrentHashMap<MessageAuthor, LinkedList<Message>> cache;
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates the cache.
*/
public MessageCache() {
lastModifications = new ConcurrentHashMap<>();
cache = new ConcurrentHashMap<>();
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
// TODO config
Bot.asyncPool.scheduleAtFixedRate(this::clean, -1, -1, TimeUnit.MINUTES);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do this here; I would never expect constructing an object to begin an irreversible scheduled task.

}

@Override
public void onMessageCreate(MessageCreateEvent event) {
if (!event.getMessageAuthor().isYourself()) {
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
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)))) {
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
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<Message> msgList = cache.get(msg.getAuthor());
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
msgList.offer(msg);
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
lastModifications.replace(msg.getAuthor(), Instant.now());

// TODO config
if (msgList.size() >= -1) {
msgList.poll();
}
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

} else {
LinkedList<Message> messages = new LinkedList<>();
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
messages.add(msg);
cache.put(msg.getAuthor(), messages);
lastModifications.put(msg.getAuthor(), Instant.now());
}
}

public ConcurrentHashMap<MessageAuthor, LinkedList<Message>> getCache() {
return cache;
}
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
}

Original file line number Diff line number Diff line change
@@ -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 {
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved

@Override
public void onMessageCreate(MessageCreateEvent event) {
if (!event.getMessageAuthor().isYourself() && Bot.messageCache.containsKey(event.getMessageAuthor())) {
cbrt-x marked this conversation as resolved.
Show resolved Hide resolved
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");
}
Comment on lines +18 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address these TODOs

}
}

}