Skip to content

Commit 221a63e

Browse files
authored
Merge pull request #553 from Neil-Tomar/feat/cross-channel-spam-automod
2 parents cf9421d + 1d35560 commit 221a63e

6 files changed

Lines changed: 90 additions & 37 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ public class ModerationConfig extends GuildConfigItem {
2828
private long adminRoleId = 0;
2929
private long expertRoleId = 0;
3030

31+
/**
32+
* The time window, in seconds, that the cross-channel spam automod looks back over when
33+
* counting a user's recent messages. If this is {@code 0}, the cross-channel spam automod
34+
* is disabled.
35+
*/
36+
private int crossChannelSpamWindowSeconds = 0;
37+
38+
/**
39+
* The number of distinct channels a user must post in within
40+
* {@link #crossChannelSpamWindowSeconds} seconds before the cross-channel spam automod acts.
41+
*/
42+
private int crossChannelSpamMinChannels = 3;
3143
/**
3244
* ID of the share-knowledge channel.
3345
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public void onMessageUpdate(@NotNull MessageUpdateEvent event) {
3939
CachedMessage before;
4040
if (optional.isPresent()) {
4141
CachedMessage inCache= optional.get();
42-
before = new CachedMessage(inCache.getMessageId(), inCache.getAuthorId(), inCache.getMessageContent(), inCache.getAttachments());
42+
before = new CachedMessage(inCache.getMessageId(), inCache.getAuthorId(), inCache.getChannelId(),inCache.getMessageContent(), inCache.getAttachments());
4343
inCache.init(event.getMessage());
4444
} else {
45-
before = new CachedMessage(event.getMessageIdLong(), event.getAuthor().getIdLong(), "[unknown content]", List.of());
45+
before = new CachedMessage(event.getMessageIdLong(), event.getAuthor().getIdLong(), event.getChannel().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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ public class MessageCacheRepository {
3131
* @throws SQLException If an error occurs.
3232
*/
3333
public void insertList(@NotNull List<CachedMessage> messages) throws DataAccessException {
34-
jdbcTemplate.batchUpdate("MERGE INTO message_cache (message_id, author_id, message_content) VALUES (?, ?, ?)",
34+
jdbcTemplate.batchUpdate("MERGE INTO message_cache (message_id, author_id, channel_id, message_content) VALUES (?, ?, ?, ?)",
3535
new BatchPreparedStatementSetter() {
3636
@Override
3737
public void setValues(PreparedStatement stmt, int i) throws SQLException {
3838
CachedMessage msg = messages.get(i);
3939
stmt.setLong(1, msg.getMessageId());
4040
stmt.setLong(2, msg.getAuthorId());
41-
stmt.setString(3, msg.getMessageContent());
41+
stmt.setLong(3,msg.getChannelId());
42+
stmt.setString(4, msg.getMessageContent());
4243
stmt.executeUpdate();
4344
}
4445

@@ -88,7 +89,7 @@ public List<CachedMessage> getAll() throws DataAccessException {
8889
messages.merge(msg.getMessageId(), msg, (oldValue, value) -> {
8990
ArrayList<String> attachments = new ArrayList<>(oldValue.getAttachments());
9091
attachments.addAll(value.getAttachments());
91-
return new CachedMessage(oldValue.getMessageId(), oldValue.getAuthorId(), oldValue.getMessageContent(), attachments);
92+
return new CachedMessage(oldValue.getMessageId(), oldValue.getAuthorId(), oldValue.getChannelId(),oldValue.getMessageContent(), attachments);
9293
});
9394
}
9495
return new ArrayList<>(messages.values());
@@ -119,6 +120,7 @@ private CachedMessage read(ResultSet rs) throws SQLException {
119120
return new CachedMessage(
120121
rs.getLong("message_cache.message_id"),
121122
rs.getLong("author_id"),
123+
rs.getLong("channel_id"),
122124
rs.getString("message_content"),
123125
attachments);
124126
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,29 @@
1919
public class CachedMessage {
2020
private final long messageId;
2121
private final long authorId;
22+
private final long channelId;
2223
private String messageContent;
2324
private List<String> attachments=new ArrayList<>();
2425

25-
private CachedMessage(long messageId, long authorId) {
26+
private CachedMessage(long messageId, long authorId,long channelId) {
2627
this.messageId = messageId;
2728
this.authorId = authorId;
29+
this.channelId = channelId;
2830
}
2931

3032
/**
3133
* Creates a {@link CachedMessage} with the given information.
3234
* @param messageId The Discord ID of the message
3335
* @param authorId The Discord ID of the message author
36+
* @param channelId The Discord ID of the message channel
3437
* @param messageContent the textual content of the message
3538
* @param attachments The attachment URLs
3639
*/
37-
public CachedMessage(long messageId, long authorId, String messageContent, List<String> attachments) {
40+
public CachedMessage(long messageId, long authorId, long channelId, String messageContent, List<String> attachments) {
3841
super();
3942
this.messageId = messageId;
4043
this.authorId = authorId;
44+
this.channelId = channelId;
4145
this.messageContent = messageContent;
4246
this.attachments = List.copyOf(attachments);
4347
}
@@ -49,7 +53,7 @@ public CachedMessage(long messageId, long authorId, String messageContent, List<
4953
* @return The built {@link CachedMessage}.
5054
*/
5155
public static CachedMessage of(Message message) {
52-
CachedMessage cachedMessage = new CachedMessage(message.getIdLong(), message.getAuthor().getIdLong());
56+
CachedMessage cachedMessage = new CachedMessage(message.getIdLong(), message.getAuthor().getIdLong(),message.getChannelIdLong());
5357
cachedMessage.init(message);
5458
return cachedMessage;
5559
}

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

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import lombok.extern.slf4j.Slf4j;
44
import net.discordjug.javabot.data.config.BotConfig;
55
import net.discordjug.javabot.data.h2db.message_cache.MessageCache;
6+
import net.discordjug.javabot.data.h2db.message_cache.model.CachedMessage;
67
import net.discordjug.javabot.systems.moderation.warn.model.WarnSeverity;
78
import net.discordjug.javabot.systems.notification.NotificationService;
89
import net.discordjug.javabot.util.ExceptionLogger;
@@ -24,9 +25,7 @@
2425
import java.net.URL;
2526
import java.time.Duration;
2627
import java.time.temporal.ChronoUnit;
27-
import java.util.Collections;
28-
import java.util.List;
29-
import java.util.Scanner;
28+
import java.util.*;
3029
import java.util.concurrent.TimeUnit;
3130
import java.util.regex.Matcher;
3231
import java.util.regex.Pattern;
@@ -50,18 +49,19 @@ public class AutoMod extends ListenerAdapter {
5049
private final MessageCache messageCache;
5150

5251
/**
53-
* Constructor of the class, that creates a list of strings with potential spam/scam urls.
52+
* Constructor of the class, that creates a list of strings with potential spam/scam URLs.
53+
*
5454
* @param notificationService The {@link QOTWPointsService}
55-
* @param botConfig The main configuration of the bot
56-
* @param moderationService Service object for moderating members
57-
* @param messageCache service for retrieving cached messages
55+
* @param botConfig The main configuration of the bot
56+
* @param moderationService Service object for moderating members
57+
* @param messageCache service for retrieving cached messages
5858
*/
5959
public AutoMod(NotificationService notificationService, BotConfig botConfig, ModerationService moderationService, MessageCache messageCache) {
6060
this.notificationService = notificationService;
6161
this.botConfig = botConfig;
6262
this.moderationService = moderationService;
6363
this.messageCache = messageCache;
64-
try(Scanner scan = new Scanner(new URL("https://raw.githubusercontent.com/DevSpen/scam-links/master/src/links.txt").openStream()).useDelimiter("\\A")) {
64+
try (Scanner scan = new Scanner(new URL("https://raw.githubusercontent.com/DevSpen/scam-links/master/src/links.txt").openStream()).useDelimiter("\\A")) {
6565
String response = scan.next();
6666
spamUrls = List.of(response.split("\n"));
6767
} catch (IOException e) {
@@ -105,21 +105,46 @@ private boolean canBypassAutomod(Member member) {
105105
private void checkNewMessageAutomod(@Nonnull Message message) {
106106
// spam
107107
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-
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+
118118
if (spamCount >= 5) {
119-
handleSpam(message, message.getMember());
119+
handleSpam(message);
120120
}
121-
122121
checkContentAutomod(message);
122+
checkCrossChannelSpam(message);
123+
}
124+
125+
private void checkCrossChannelSpam(@Nonnull Message message) {
126+
int spamWindowSeconds = (botConfig.get(message.getGuild()).getModerationConfig()).getCrossChannelSpamWindowSeconds();
127+
Set<Long> channelIds = new HashSet<>();
128+
List<CachedMessage> spamMessages = new ArrayList<>();
129+
130+
if (spamWindowSeconds <= 0) {
131+
return;
132+
}
133+
134+
for (CachedMessage cachedMessage : messageCache.getMessagesAfter(message.getTimeCreated().minusSeconds(spamWindowSeconds))) {
135+
if (cachedMessage.getMessageId() == message.getIdLong()) {
136+
continue;
137+
}
138+
if (cachedMessage.getAuthorId() != message.getAuthor().getIdLong()) {
139+
continue;
140+
}
141+
channelIds.add(cachedMessage.getChannelId());
142+
spamMessages.add(cachedMessage);
143+
}
144+
145+
if (channelIds.size() >= (botConfig.get(message.getGuild()).getModerationConfig()).getCrossChannelSpamMinChannels()) {
146+
handleSpam(spamMessages, message);
147+
}
123148
}
124149

125150
/**
@@ -130,7 +155,7 @@ private void checkNewMessageAutomod(@Nonnull Message message) {
130155
private void checkContentAutomod(@Nonnull Message message) {
131156
//Check for Advertising Links
132157
if (hasAdvertisingLink(message)) {
133-
doAutomodActions(message,"Advertising");
158+
doAutomodActions(message, "Advertising");
134159
}
135160

136161
//Check for suspicious Links
@@ -158,20 +183,29 @@ private void doAutomodActions(Message message, String reason) {
158183
/**
159184
* Handles detected spam messages.
160185
*
161-
* @param msg the (last) spam message
162-
* @param member the member to be potentially warned
186+
* @param msg the (last) spam message
163187
*/
164-
private void handleSpam(@Nonnull Message msg, Member member) {
188+
private void handleSpam(@Nonnull Message msg) {
189+
timeoutForSpam(msg);
190+
msg.delete().queue();
191+
}
192+
193+
private void handleSpam(@Nonnull List<CachedMessage> cachedMessages, Message message) {
194+
timeoutForSpam(message);
195+
cachedMessages.forEach(cachedMessage -> message.getGuild().getTextChannelById(cachedMessage.getChannelId())
196+
.deleteMessageById(cachedMessage.getMessageId()).queue());
197+
}
198+
199+
private void timeoutForSpam(@Nonnull Message message) {
165200
moderationService
166201
.timeout(
167-
member.getUser(),
202+
message.getAuthor(),
168203
"Automod: Spam",
169-
msg.getGuild().getSelfMember(),
204+
message.getGuild().getSelfMember(),
170205
Duration.of(6, ChronoUnit.HOURS),
171-
msg.getChannel(),
206+
message.getChannel(),
172207
false
173208
);
174-
msg.delete().queue();
175209
}
176210

177211
/**
@@ -237,5 +271,5 @@ private boolean isSuggestionsChannel(@NotNull MessageChannelUnion channel) {
237271
return channel.getType().isGuild() &&
238272
channel.getIdLong() == botConfig.get(channel.asGuildMessageChannel().getGuild()).getModerationConfig().getSuggestionChannel().getIdLong();
239273
}
240-
274+
241275
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE message_cache ADD COLUMN channel_id BIGINT DEFAULT -1;

0 commit comments

Comments
 (0)