Skip to content

Commit 2842442

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

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
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,92 @@
1-
@rem
2-
@rem Copyright 2015 the original author or authors.
3-
@rem
4-
@rem Licensed under the Apache License, Version 2.0 (the "License");
5-
@rem you may not use this file except in compliance with the License.
6-
@rem You may obtain a copy of the License at
7-
@rem
8-
@rem https://www.apache.org/licenses/LICENSE-2.0
9-
@rem
10-
@rem Unless required by applicable law or agreed to in writing, software
11-
@rem distributed under the License is distributed on an "AS IS" BASIS,
12-
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
@rem See the License for the specific language governing permissions and
14-
@rem limitations under the License.
15-
@rem
16-
17-
@if "%DEBUG%"=="" @echo off
18-
@rem ##########################################################################
19-
@rem
20-
@rem Gradle startup script for Windows
21-
@rem
22-
@rem ##########################################################################
23-
24-
@rem Set local scope for the variables with windows NT shell
25-
if "%OS%"=="Windows_NT" setlocal
26-
27-
set DIRNAME=%~dp0
28-
if "%DIRNAME%"=="" set DIRNAME=.
29-
@rem This is normally unused
30-
set APP_BASE_NAME=%~n0
31-
set APP_HOME=%DIRNAME%
32-
33-
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
34-
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35-
36-
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37-
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38-
39-
@rem Find java.exe
40-
if defined JAVA_HOME goto findJavaFromJavaHome
41-
42-
set JAVA_EXE=java.exe
43-
%JAVA_EXE% -version >NUL 2>&1
44-
if %ERRORLEVEL% equ 0 goto execute
45-
46-
echo.
47-
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48-
echo.
49-
echo Please set the JAVA_HOME variable in your environment to match the
50-
echo location of your Java installation.
51-
52-
goto fail
53-
54-
:findJavaFromJavaHome
55-
set JAVA_HOME=%JAVA_HOME:"=%
56-
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57-
58-
if exist "%JAVA_EXE%" goto execute
59-
60-
echo.
61-
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62-
echo.
63-
echo Please set the JAVA_HOME variable in your environment to match the
64-
echo location of your Java installation.
65-
66-
goto fail
67-
68-
:execute
69-
@rem Setup the command line
70-
71-
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72-
73-
74-
@rem Execute Gradle
75-
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76-
77-
:end
78-
@rem End local scope for the variables with windows NT shell
79-
if %ERRORLEVEL% equ 0 goto mainEnd
80-
81-
:fail
82-
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83-
rem the _cmd.exe /c_ return code!
84-
set EXIT_CODE=%ERRORLEVEL%
85-
if %EXIT_CODE% equ 0 set EXIT_CODE=1
86-
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87-
exit /b %EXIT_CODE%
88-
89-
:mainEnd
90-
if "%OS%"=="Windows_NT" endlocal
91-
92-
:omega
1+
@rem
2+
@rem Copyright 2015 the original author or authors.
3+
@rem
4+
@rem Licensed under the Apache License, Version 2.0 (the "License");
5+
@rem you may not use this file except in compliance with the License.
6+
@rem You may obtain a copy of the License at
7+
@rem
8+
@rem https://www.apache.org/licenses/LICENSE-2.0
9+
@rem
10+
@rem Unless required by applicable law or agreed to in writing, software
11+
@rem distributed under the License is distributed on an "AS IS" BASIS,
12+
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
@rem See the License for the specific language governing permissions and
14+
@rem limitations under the License.
15+
@rem
16+
17+
@if "%DEBUG%"=="" @echo off
18+
@rem ##########################################################################
19+
@rem
20+
@rem Gradle startup script for Windows
21+
@rem
22+
@rem ##########################################################################
23+
24+
@rem Set local scope for the variables with windows NT shell
25+
if "%OS%"=="Windows_NT" setlocal
26+
27+
set DIRNAME=%~dp0
28+
if "%DIRNAME%"=="" set DIRNAME=.
29+
@rem This is normally unused
30+
set APP_BASE_NAME=%~n0
31+
set APP_HOME=%DIRNAME%
32+
33+
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
34+
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35+
36+
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37+
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38+
39+
@rem Find java.exe
40+
if defined JAVA_HOME goto findJavaFromJavaHome
41+
42+
set JAVA_EXE=java.exe
43+
%JAVA_EXE% -version >NUL 2>&1
44+
if %ERRORLEVEL% equ 0 goto execute
45+
46+
echo.
47+
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48+
echo.
49+
echo Please set the JAVA_HOME variable in your environment to match the
50+
echo location of your Java installation.
51+
52+
goto fail
53+
54+
:findJavaFromJavaHome
55+
set JAVA_HOME=%JAVA_HOME:"=%
56+
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57+
58+
if exist "%JAVA_EXE%" goto execute
59+
60+
echo.
61+
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62+
echo.
63+
echo Please set the JAVA_HOME variable in your environment to match the
64+
echo location of your Java installation.
65+
66+
goto fail
67+
68+
:execute
69+
@rem Setup the command line
70+
71+
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72+
73+
74+
@rem Execute Gradle
75+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76+
77+
:end
78+
@rem End local scope for the variables with windows NT shell
79+
if %ERRORLEVEL% equ 0 goto mainEnd
80+
81+
:fail
82+
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83+
rem the _cmd.exe /c_ return code!
84+
set EXIT_CODE=%ERRORLEVEL%
85+
if %EXIT_CODE% equ 0 set EXIT_CODE=1
86+
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87+
exit /b %EXIT_CODE%
88+
89+
:mainEnd
90+
if "%OS%"=="Windows_NT" endlocal
91+
92+
:omega

0 commit comments

Comments
 (0)