diff --git a/application/config.json.template b/application/config.json.template index 8f64df1a15..253bf483ba 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -45,7 +45,7 @@ "steamcommunity", "freenitro", "usd", - "earn", + "^earn", ".exe" ], "hostWhitelist": [ diff --git a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetector.java b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetector.java index 654cd87084..c1181fa8fe 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetector.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetector.java @@ -114,7 +114,13 @@ private boolean containsSuspiciousKeyword(String token) { return config.getSuspiciousKeywords() .stream() .map(keyword -> keyword.toLowerCase(Locale.US)) - .anyMatch(preparedToken::contains); + .anyMatch(keyword -> { + // Simple regex-inspired syntax "^foo" + if (startsWith(keyword, '^')) { + return preparedToken.startsWith(keyword.substring(1)); + } + return preparedToken.contains(keyword); + }); } private boolean isHostSimilarToKeyword(String host, String keyword) { @@ -140,6 +146,10 @@ private boolean isHostSimilarToKeyword(String host, String keyword) { return false; } + private static boolean startsWith(CharSequence text, char prefixToTest) { + return !text.isEmpty() && text.charAt(0) == prefixToTest; + } + private static class AnalyseResults { private boolean pingsEveryone; private boolean containsSuspiciousKeyword; diff --git a/application/src/test/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetectorTest.java b/application/src/test/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetectorTest.java index f62dd31646..e55a90f717 100644 --- a/application/src/test/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetectorTest.java +++ b/application/src/test/java/org/togetherjava/tjbot/features/moderation/scam/ScamDetectorTest.java @@ -26,10 +26,10 @@ void setUp() { ScamBlockerConfig scamConfig = mock(ScamBlockerConfig.class); when(config.getScamBlocker()).thenReturn(scamConfig); - when(scamConfig.getSuspiciousKeywords()) - .thenReturn(Set.of("nitro", "boob", "sexy", "sexi", "esex", "steam", "gift", "onlyfans", - "bitcoin", "btc", "promo", "trader", "trading", "whatsapp", "crypto", "claim", - "teen", "adobe", "hack", "steamcommunity", "freenitro", "usd", "earn", ".exe")); + when(scamConfig.getSuspiciousKeywords()).thenReturn(Set.of("nitro", "boob", "sexy", "sexi", + "esex", "steam", "gift", "onlyfans", "bitcoin", "btc", "promo", "trader", "trading", + "whatsapp", "crypto", "claim", "teen", "adobe", "hack", "steamcommunity", + "freenitro", "usd", "^earn", ".exe")); when(scamConfig.getHostWhitelist()).thenReturn(Set.of("discord.com", "discord.media", "discordapp.com", "discordapp.net", "discordstatus.com")); when(scamConfig.getHostBlacklist()).thenReturn(Set.of("bit.ly", "discord.gg", "teletype.in", @@ -54,6 +54,18 @@ void detectsRealScam(String scamMessage) { assertTrue(isScamResult); } + @ParameterizedTest + @MethodSource("provideRealFalsePositiveMessages") + @DisplayName("Can ignore real false positive messages") + void ignoresFalsePositives(String falsePositiveMessage) { + // GIVEN a real false positive message + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(falsePositiveMessage); + + // THEN does not flag it as scam + assertFalse(isScamResult); + } + @Test @DisplayName("Can detect messages that contain blacklisted websites as scam") void detectsBlacklistedWebsite() { @@ -227,4 +239,10 @@ private static List provideRealScamMessages() { "Urgently looking for mods & collab managers https://discord.gg/cryptohireo", "Check this - https://transfer.sh/get/ajmkh3l7tzop/Setup.exe"); } + + private static List provideRealFalsePositiveMessages() { + return List + .of(""" + https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types"""); + } }