diff --git a/application/config.json.template b/application/config.json.template index 02835ca9e0..555bfce5f9 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -22,6 +22,7 @@ "mode": "AUTO_DELETE_BUT_APPROVE_QUARANTINE", "reportChannelPattern": "commands", "botTrapChannelPattern": "bot-trap", + "trustedUserRolePattern": "Top Helpers .+|Moderator|Community Ambassador|Expert", "suspiciousKeywords": [ "nitro", "boob", diff --git a/application/src/main/java/org/togetherjava/tjbot/config/ScamBlockerConfig.java b/application/src/main/java/org/togetherjava/tjbot/config/ScamBlockerConfig.java index 0321a8a7b8..b93119022e 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/ScamBlockerConfig.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/ScamBlockerConfig.java @@ -18,6 +18,7 @@ public final class ScamBlockerConfig { private final Mode mode; private final String reportChannelPattern; private final String botTrapChannelPattern; + private final String trustedUserRolePattern; private final Set suspiciousKeywords; private final Set hostWhitelist; private final Set hostBlacklist; @@ -32,6 +33,8 @@ private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mo required = true) String reportChannelPattern, @JsonProperty(value = "botTrapChannelPattern", required = true) String botTrapChannelPattern, + @JsonProperty(value = "trustedUserRolePattern", + required = true) String trustedUserRolePattern, @JsonProperty(value = "suspiciousKeywords", required = true) Set suspiciousKeywords, @JsonProperty(value = "hostWhitelist", required = true) Set hostWhitelist, @@ -47,6 +50,7 @@ private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mo this.mode = Objects.requireNonNull(mode); this.reportChannelPattern = Objects.requireNonNull(reportChannelPattern); this.botTrapChannelPattern = Objects.requireNonNull(botTrapChannelPattern); + this.trustedUserRolePattern = Objects.requireNonNull(trustedUserRolePattern); this.suspiciousKeywords = new HashSet<>(Objects.requireNonNull(suspiciousKeywords)); this.hostWhitelist = new HashSet<>(Objects.requireNonNull(hostWhitelist)); this.hostBlacklist = new HashSet<>(Objects.requireNonNull(hostBlacklist)); @@ -86,6 +90,15 @@ public String getBotTrapChannelPattern() { return botTrapChannelPattern; } + /** + * Gets the REGEX pattern used to identify roles that will be ignored for scam detection. + * + * @return the REGEX pattern + */ + public String getTrustedUserRolePattern() { + return trustedUserRolePattern; + } + /** * Gets the set of keywords that are considered suspicious if they appear in a message. * 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 035de5ca0c..d5198d91e6 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 @@ -1,6 +1,8 @@ package org.togetherjava.tjbot.features.moderation.scam; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.ScamBlockerConfig; @@ -24,6 +26,7 @@ public final class ScamDetector { private static final Pattern TOKENIZER = Pattern.compile("[\\s,]"); private final ScamBlockerConfig config; private final Predicate isSuspiciousAttachmentName; + private final Predicate hasTrustedRole; /** * Creates a new instance with the given configuration @@ -32,9 +35,12 @@ public final class ScamDetector { */ public ScamDetector(Config config) { this.config = config.getScamBlocker(); + isSuspiciousAttachmentName = - Pattern.compile(config.getScamBlocker().getSuspiciousAttachmentNamePattern()) + Pattern.compile(this.config.getSuspiciousAttachmentNamePattern()) .asMatchPredicate(); + hasTrustedRole = + Pattern.compile(this.config.getTrustedUserRolePattern()).asMatchPredicate(); } /** @@ -44,6 +50,13 @@ public ScamDetector(Config config) { * @return Whether the message classifies as scam */ public boolean isScam(Message message) { + Member author = message.getMember(); + boolean isTrustedUser = author != null + && author.getRoles().stream().map(Role::getName).noneMatch(hasTrustedRole); + if (isTrustedUser) { + return false; + } + String content = message.getContentDisplay(); List attachments = message.getAttachments(); 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 19127401b6..85005a3f76 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 @@ -1,6 +1,8 @@ package org.togetherjava.tjbot.features.moderation.scam; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -49,6 +51,8 @@ void setUp() { when(scamConfig.getSuspiciousAttachmentNamePattern()) .thenReturn(SUSPICIOUS_ATTACHMENT_NAME); + when(scamConfig.getTrustedUserRolePattern()).thenReturn("Moderator"); + scamDetector = new ScamDetector(config); } @@ -204,6 +208,23 @@ void ignoresHarmlessAttachments() { assertFalse(isScamResult); } + @Test + @DisplayName("Suspicious messages send by trusted users are not flagged") + void ignoreTrustedUser() { + // GIVEN a scam message send by a trusted user + String content = "Checkout https://bit.ly/3IhcLiO to get your free nitro !"; + Member trustedUser = createAuthorMock(List.of("Moderator")); + Message message = createMessageMock(content, List.of()); + + when(message.getMember()).thenReturn(trustedUser); + + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(message); + + // THEN flags it as harmless + assertTrue(isScamResult); + } + private static Message createMessageMock(String content, List attachments) { Message message = mock(Message.class); when(message.getContentRaw()).thenReturn(content); @@ -219,6 +240,19 @@ private static Message.Attachment createImageAttachmentMock(String name) { return attachment; } + private static Member createAuthorMock(List roleNames) { + List roles = new ArrayList<>(); + for (String roleName : roleNames) { + Role role = mock(Role.class); + when(role.getName()).thenReturn(roleName); + roles.add(role); + } + + Member member = mock(Member.class); + when(member.getRoles()).thenReturn(roles); + return member; + } + private static List provideRealScamMessages() { return List.of(""" 🤩bro steam gived nitro - https://nitro-ds.online/LfgUfMzqYyx12""",