Skip to content

Commit c160a4d

Browse files
committed
[feature/handle-similar-messages-as-scam] Done
1 parent 9ff5cb8 commit c160a4d

File tree

5 files changed

+179
-104
lines changed

5 files changed

+179
-104
lines changed

application/config.json.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"hostWhitelist": ["discord.com", "discord.gg", "discord.media", "discordapp.com", "discordapp.net", "discordstatus.com"],
2525
"hostBlacklist": ["bit.ly"],
2626
"suspiciousHostKeywords": ["discord", "nitro", "premium"],
27-
"isHostSimilarToKeywordDistanceThreshold": 2
27+
"isHostSimilarToKeywordDistanceThreshold": 2,
28+
"maxSimilarMessages": 2
2829
},
2930
"wolframAlphaAppId": "79J52T-6239TVXHR7",
3031
"helpSystem": {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.togetherjava.tjbot.config;
2+
3+
import java.time.Instant;
4+
import java.util.Objects;
5+
6+
/**
7+
* Information about a message, used to detect spam of the same message by the same user in
8+
* different channels.
9+
*
10+
* @param userId the id of the user
11+
* @param channelId the channel where the message was posted
12+
* @param messageHash the hash of the message
13+
* @param timestamp when the message was posted
14+
*/
15+
public record MessageInfo(long userId, long channelId, int messageHash, Instant timestamp) {
16+
@Override
17+
public boolean equals(Object other) {
18+
return other instanceof MessageInfo message && this.userId == message.userId
19+
&& this.channelId == message.channelId;
20+
}
21+
22+
@Override
23+
public int hashCode() {
24+
return Objects.hash(userId, channelId);
25+
}
26+
}

application/src/main/java/org/togetherjava/tjbot/config/ScamBlockerConfig.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class ScamBlockerConfig {
2222
private final Set<String> hostBlacklist;
2323
private final Set<String> suspiciousHostKeywords;
2424
private final int isHostSimilarToKeywordDistanceThreshold;
25+
private final int maxSimilarMessages;
2526

2627
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
2728
private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mode,
@@ -34,14 +35,16 @@ private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mo
3435
@JsonProperty(value = "suspiciousHostKeywords",
3536
required = true) Set<String> suspiciousHostKeywords,
3637
@JsonProperty(value = "isHostSimilarToKeywordDistanceThreshold",
37-
required = true) int isHostSimilarToKeywordDistanceThreshold) {
38+
required = true) int isHostSimilarToKeywordDistanceThreshold,
39+
@JsonProperty(value = "maxSimilarMessages") int maxSimilarMessages) {
3840
this.mode = Objects.requireNonNull(mode);
3941
this.reportChannelPattern = Objects.requireNonNull(reportChannelPattern);
4042
this.suspiciousKeywords = new HashSet<>(Objects.requireNonNull(suspiciousKeywords));
4143
this.hostWhitelist = new HashSet<>(Objects.requireNonNull(hostWhitelist));
4244
this.hostBlacklist = new HashSet<>(Objects.requireNonNull(hostBlacklist));
4345
this.suspiciousHostKeywords = new HashSet<>(Objects.requireNonNull(suspiciousHostKeywords));
4446
this.isHostSimilarToKeywordDistanceThreshold = isHostSimilarToKeywordDistanceThreshold;
47+
this.maxSimilarMessages = maxSimilarMessages;
4548
}
4649

4750
/**
@@ -111,6 +114,15 @@ public int getIsHostSimilarToKeywordDistanceThreshold() {
111114
return isHostSimilarToKeywordDistanceThreshold;
112115
}
113116

117+
/**
118+
* Gets the maximum amount of allowed messages before it gets flagged by the scam detector.
119+
*
120+
* @return the maximum amount of allowed messages
121+
*/
122+
public int getMaxSimilarMessages() {
123+
return maxSimilarMessages;
124+
}
125+
114126
/**
115127
* Mode of a scam blocker. Controls which actions it takes when detecting scam.
116128
*/

application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
import org.slf4j.LoggerFactory;
2525

2626
import org.togetherjava.tjbot.config.Config;
27+
import org.togetherjava.tjbot.config.MessageInfo;
2728
import org.togetherjava.tjbot.config.ScamBlockerConfig;
28-
import org.togetherjava.tjbot.features.MessageReceiverAdapter;
29-
import org.togetherjava.tjbot.features.UserInteractionType;
30-
import org.togetherjava.tjbot.features.UserInteractor;
29+
import org.togetherjava.tjbot.features.*;
3130
import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator;
3231
import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor;
3332
import org.togetherjava.tjbot.features.moderation.ModerationAction;
@@ -38,11 +37,10 @@
3837
import org.togetherjava.tjbot.logging.LogMarkers;
3938

4039
import java.awt.Color;
41-
import java.util.Collection;
42-
import java.util.EnumSet;
43-
import java.util.List;
44-
import java.util.Optional;
45-
import java.util.Set;
40+
import java.time.Instant;
41+
import java.time.temporal.ChronoUnit;
42+
import java.util.*;
43+
import java.util.concurrent.TimeUnit;
4644
import java.util.function.Consumer;
4745
import java.util.function.Predicate;
4846
import java.util.function.UnaryOperator;
@@ -55,7 +53,7 @@
5553
* If scam is detected, depending on the configuration, the blockers actions range from deleting the
5654
* message and banning the author to just logging the message for auditing.
5755
*/
58-
public final class ScamBlocker extends MessageReceiverAdapter implements UserInteractor {
56+
public final class ScamBlocker extends MessageReceiverAdapter implements UserInteractor, Routine {
5957
private static final Logger logger = LoggerFactory.getLogger(ScamBlocker.class);
6058
private static final Color AMBIENT_COLOR = Color.decode("#CFBFF5");
6159
private static final Set<ScamBlockerConfig.Mode> MODES_WITH_IMMEDIATE_DELETION =
@@ -72,6 +70,7 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt
7270
private final Predicate<String> hasRequiredRole;
7371

7472
private final ComponentIdInteractor componentIdInteractor;
73+
private final Set<MessageInfo> messageCache;
7574

7675
/**
7776
* Creates a new listener to receive all message sent in any channel.
@@ -95,6 +94,7 @@ public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHis
9594
hasRequiredRole = Pattern.compile(config.getSoftModerationRolePattern()).asMatchPredicate();
9695

9796
componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName());
97+
messageCache = new HashSet<>();
9898
}
9999

100100
@Override
@@ -124,7 +124,7 @@ public void onMessageReceived(MessageReceivedEvent event) {
124124

125125
Message message = event.getMessage();
126126
String content = message.getContentDisplay();
127-
if (!scamDetector.isScam(content)) {
127+
if (!scamDetector.isScam(content) && !doSimilarMessageCheck(event)) {
128128
return;
129129
}
130130

@@ -136,6 +136,42 @@ public void onMessageReceived(MessageReceivedEvent event) {
136136
takeAction(event);
137137
}
138138

139+
@Override
140+
public Schedule createSchedule() {
141+
return new Schedule(ScheduleMode.FIXED_RATE, 1, 1, TimeUnit.HOURS);
142+
}
143+
144+
@Override
145+
public void runRoutine(JDA jda) {
146+
Instant now = Instant.now();
147+
messageCache.removeIf(m -> m.timestamp().plus(1, ChronoUnit.HOURS).isBefore(now));
148+
}
149+
150+
/**
151+
* Stores message data and if many messages of same author, different channel and same content
152+
* is posted several times, returns true.
153+
*
154+
* @param event the message event
155+
* @return true if the user spammed the message in several channels, false otherwise
156+
*/
157+
private boolean doSimilarMessageCheck(MessageReceivedEvent event) {
158+
long userId = event.getAuthor().getIdLong();
159+
long channelId = event.getChannel().getIdLong();
160+
int messageHash = getHash(event.getMessage());
161+
Instant timestamp = event.getMessage().getTimeCreated().toInstant();
162+
messageCache.add(new MessageInfo(userId, channelId, messageHash, timestamp));
163+
return config.getScamBlocker().getMaxSimilarMessages() < messageCache.stream()
164+
.filter(m -> m.userId() == userId && m.messageHash() == messageHash)
165+
.count();
166+
}
167+
168+
private int getHash(Message message) {
169+
return message.getContentRaw().hashCode() + message.getAttachments()
170+
.stream()
171+
.mapToInt(a -> a.getFileName().hashCode())
172+
.reduce(1, (a, b) -> a * b);
173+
}
174+
139175
private void takeActionWasAlreadyReported(MessageReceivedEvent event) {
140176
// The user recently send the same scam already, and that was already reported and handled
141177
addScamToHistory(event);

gradlew.bat

Lines changed: 92 additions & 92 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)