diff --git a/application/build.gradle b/application/build.gradle index 78c3254c47..15cd32dcbb 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -77,7 +77,7 @@ dependencies { implementation 'org.kohsuke:github-api:1.327' - implementation 'org.apache.commons:commons-text:1.13.1' + implementation 'org.apache.commons:commons-text:1.14.0' implementation 'com.apptasticsoftware:rssreader:3.9.3' testImplementation 'org.mockito:mockito-core:5.18.0' diff --git a/application/config.json.template b/application/config.json.template index 3e568329a1..0c0989ba96 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", @@ -38,21 +39,27 @@ "trading", "whatsapp", "crypto", - "claim", + "^claim", "teen", "adobe", "hack", "steamcommunity", "freenitro", - "^earn", - ".exe" + "^earn$", + "^earning", + ".exe$" ], "hostWhitelist": [ "discord.com", "discord.media", "discordapp.com", "discordapp.net", - "discordstatus.com" + "discordstatus.com", + "thehackernews.com", + "gradle.org", + "help.gradle.org", + "youtube.com", + "www.youtube.com" ], "hostBlacklist": [ "bit.ly", @@ -66,7 +73,8 @@ "telegra.ph", "shorturl.at", "cheatings.xyz", - "transfer.sh" + "transfer.sh", + "tobimoller.space" ], "suspiciousHostKeywords": [ "discord", @@ -75,9 +83,12 @@ "free", "cheat", "crypto", - "tele" + "telegra", + "telety" ], - "isHostSimilarToKeywordDistanceThreshold": 2 + "isHostSimilarToKeywordDistanceThreshold": 2, + "suspiciousAttachmentsThreshold": 3, + "suspiciousAttachmentNamePattern": "(image|\\d{1,2})\\.[^.]{0,5}" }, "wolframAlphaAppId": "79J52T-6239TVXHR7", "helpSystem": { 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 d95d9f9cce..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,11 +18,14 @@ 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; private final Set suspiciousHostKeywords; private final int isHostSimilarToKeywordDistanceThreshold; + private final int suspiciousAttachmentsThreshold; + private final String suspiciousAttachmentNamePattern; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mode, @@ -30,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, @@ -37,15 +42,23 @@ private ScamBlockerConfig(@JsonProperty(value = "mode", required = true) Mode mo @JsonProperty(value = "suspiciousHostKeywords", required = true) Set suspiciousHostKeywords, @JsonProperty(value = "isHostSimilarToKeywordDistanceThreshold", - required = true) int isHostSimilarToKeywordDistanceThreshold) { + required = true) int isHostSimilarToKeywordDistanceThreshold, + @JsonProperty(value = "suspiciousAttachmentsThreshold", + required = true) int suspiciousAttachmentsThreshold, + @JsonProperty(value = "suspiciousAttachmentNamePattern", + required = true) String suspiciousAttachmentNamePattern) { 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)); this.suspiciousHostKeywords = new HashSet<>(Objects.requireNonNull(suspiciousHostKeywords)); this.isHostSimilarToKeywordDistanceThreshold = isHostSimilarToKeywordDistanceThreshold; + this.suspiciousAttachmentsThreshold = suspiciousAttachmentsThreshold; + this.suspiciousAttachmentNamePattern = + Objects.requireNonNull(suspiciousAttachmentNamePattern); } /** @@ -77,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. * @@ -125,6 +147,26 @@ public int getIsHostSimilarToKeywordDistanceThreshold() { return isHostSimilarToKeywordDistanceThreshold; } + /** + * Gets the minimum amount of suspicious attachments that are required in a message to flag it + * as suspicious for its contained attachments. + * + * @return the minimum amount of suspicious attachments + */ + public int getSuspiciousAttachmentsThreshold() { + return suspiciousAttachmentsThreshold; + } + + /** + * Gets the REGEX pattern used to identify an attachment file name that is considered + * suspicious. The file name includes the extension. + * + * @return the attachment file name pattern + */ + public String getSuspiciousAttachmentNamePattern() { + return suspiciousAttachmentNamePattern; + } + /** * Mode of a scam blocker. Controls which actions it takes when detecting scam. */ diff --git a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java index 0a5add113b..5dbbdbf8b2 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/basic/SuggestionsUpDownVoter.java @@ -23,9 +23,9 @@ */ public final class SuggestionsUpDownVoter extends MessageReceiverAdapter { private static final Logger logger = LoggerFactory.getLogger(SuggestionsUpDownVoter.class); - private static final int TITLE_MAX_LENGTH = 60; private static final Emoji FALLBACK_UP_VOTE = Emoji.fromUnicode("👍"); private static final Emoji FALLBACK_DOWN_VOTE = Emoji.fromUnicode("👎"); + private static final int THREAD_TITLE_MAX_LENGTH = 60; private final SuggestionsConfig config; @@ -55,19 +55,33 @@ public void onMessageReceived(MessageReceivedEvent event) { } private static void createThread(Message message) { - String title = message.getContentRaw(); + String threadTitle = generateThreadTitle(message); + message.createThreadChannel(threadTitle).queue(); + } - if (title.length() >= TITLE_MAX_LENGTH) { - int lastWordEnd = title.lastIndexOf(' ', TITLE_MAX_LENGTH); + /** + * Generates a title for the given message. The maximum length of the title is + * {@value #THREAD_TITLE_MAX_LENGTH}. + * + * @param message The message for which to generate the title. + * @return The generated and truncated thread title. + */ + private static String generateThreadTitle(Message message) { + String primaryTitle = message.getContentStripped(); + String fallbackTitle = message.getAuthor().getEffectiveName() + "'s suggestion"; + String title = primaryTitle.isEmpty() ? fallbackTitle : primaryTitle; - if (lastWordEnd == -1) { - lastWordEnd = TITLE_MAX_LENGTH; - } + if (title.length() <= THREAD_TITLE_MAX_LENGTH) { + return title; + } + + int lastWordEnd = title.lastIndexOf(' ', THREAD_TITLE_MAX_LENGTH); - title = title.substring(0, lastWordEnd); + if (lastWordEnd == -1) { + return title.substring(0, THREAD_TITLE_MAX_LENGTH); } - message.createThreadChannel(title).queue(); + return title.substring(0, lastWordEnd); } private static void reactWith(String emojiName, Emoji fallbackEmoji, Guild guild, diff --git a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java index 057a71cee6..730d7eef14 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java @@ -137,8 +137,7 @@ public void onMessageReceived(MessageReceivedEvent event) { } Message message = event.getMessage(); - String content = message.getContentDisplay(); - if (isSafe && scamDetector.isScam(content)) { + if (isSafe && scamDetector.isScam(message)) { isSafe = false; } 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 33a9a28743..3240530f50 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,11 +1,19 @@ 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; import org.togetherjava.tjbot.features.utils.StringDistances; import java.net.URI; +import java.util.Collection; +import java.util.List; import java.util.Locale; +import java.util.StringJoiner; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -18,6 +26,8 @@ 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 @@ -26,6 +36,36 @@ public final class ScamDetector { */ public ScamDetector(Config config) { this.config = config.getScamBlocker(); + + isSuspiciousAttachmentName = + Pattern.compile(this.config.getSuspiciousAttachmentNamePattern()) + .asMatchPredicate(); + hasTrustedRole = + Pattern.compile(this.config.getTrustedUserRolePattern()).asMatchPredicate(); + } + + /** + * Detects whether the given message classifies as scam or not, using certain heuristics. + * + * @param message the message to analyze + * @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(); + + if (content.isBlank()) { + return areAttachmentsSuspicious(attachments); + } + + return isScam(content); } /** @@ -115,14 +155,32 @@ private boolean containsSuspiciousKeyword(String token) { .stream() .map(keyword -> keyword.toLowerCase(Locale.US)) .anyMatch(keyword -> { + // Exact match "^foo$" + if (startsWith(keyword, '^') && endsWith(keyword, '$')) { + return preparedToken.equals(keyword.substring(1, keyword.length() - 1)); + } // Simple regex-inspired syntax "^foo" if (startsWith(keyword, '^')) { return preparedToken.startsWith(keyword.substring(1)); } + // Simple regex-inspired syntax "foo$" + if (endsWith(keyword, '$')) { + return preparedToken.endsWith(keyword.substring(0, keyword.length() - 1)); + } return preparedToken.contains(keyword); }); } + private boolean areAttachmentsSuspicious(Collection attachments) { + long suspiciousAttachments = + attachments.stream().filter(this::isAttachmentSuspicious).count(); + return suspiciousAttachments >= config.getSuspiciousAttachmentsThreshold(); + } + + private boolean isAttachmentSuspicious(Message.Attachment attachment) { + return attachment.isImage() && isSuspiciousAttachmentName.test(attachment.getFileName()); + } + private boolean isHostSimilarToKeyword(String host, String keyword) { // NOTE This algorithm is far from optimal. // It is good enough for our purpose though and not that complex. @@ -150,11 +208,26 @@ private static boolean startsWith(CharSequence text, char prefixToTest) { return !text.isEmpty() && text.charAt(0) == prefixToTest; } + private static boolean endsWith(CharSequence text, char suffixToTest) { + return !text.isEmpty() && text.charAt(text.length() - 1) == suffixToTest; + } + private static class AnalyseResults { private boolean pingsEveryone; private boolean containsSuspiciousKeyword; private boolean containsDollarSign; private boolean hasUrl; private boolean hasSuspiciousUrl; + + @Override + public String toString() { + return new StringJoiner(", ", AnalyseResults.class.getSimpleName() + "[", "]") + .add("pingsEveryone=" + pingsEveryone) + .add("containsSuspiciousKeyword=" + containsSuspiciousKeyword) + .add("containsDollarSign=" + containsDollarSign) + .add("hasUrl=" + hasUrl) + .add("hasSuspiciousUrl=" + hasSuspiciousUrl) + .toString(); + } } } 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 ba1ea14f08..609e4beeb4 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,5 +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; @@ -9,6 +12,8 @@ import org.togetherjava.tjbot.config.Config; import org.togetherjava.tjbot.config.ScamBlockerConfig; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -18,6 +23,9 @@ import static org.mockito.Mockito.when; final class ScamDetectorTest { + private static final int SUSPICIOUS_ATTACHMENTS_THRESHOLD = 3; + private static final String SUSPICIOUS_ATTACHMENT_NAME = "scam.png"; + private ScamDetector scamDetector; @BeforeEach @@ -26,18 +34,25 @@ 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", "^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", "^earn$", "^earning", ".exe$")); when(scamConfig.getHostWhitelist()).thenReturn(Set.of("discord.com", "discord.media", - "discordapp.com", "discordapp.net", "discordstatus.com")); + "discordapp.com", "discordapp.net", "discordstatus.com", "thehackernews.com", + "gradle.org", "help.gradle.org", "youtube.com", "www.youtube.com")); when(scamConfig.getHostBlacklist()).thenReturn(Set.of("bit.ly", "discord.gg", "teletype.in", "t.me", "corematrix.us", "u.to", "steamcommunity.com", "goo.su", "telegra.ph", - "shorturl.at", "cheatings.xyz", "transfer.sh")); - when(scamConfig.getSuspiciousHostKeywords()) - .thenReturn(Set.of("discord", "nitro", "premium", "free", "cheat", "crypto", "tele")); + "shorturl.at", "cheatings.xyz", "transfer.sh", "tobimoller.space")); + when(scamConfig.getSuspiciousHostKeywords()).thenReturn(Set.of("discord", "nitro", + "premium", "free", "cheat", "crypto", "telegra", "telety")); when(scamConfig.getIsHostSimilarToKeywordDistanceThreshold()).thenReturn(2); + when(scamConfig.getSuspiciousAttachmentsThreshold()) + .thenReturn(SUSPICIOUS_ATTACHMENTS_THRESHOLD); + when(scamConfig.getSuspiciousAttachmentNamePattern()) + .thenReturn(SUSPICIOUS_ATTACHMENT_NAME); + + when(scamConfig.getTrustedUserRolePattern()).thenReturn("Moderator"); scamDetector = new ScamDetector(config); } @@ -121,6 +136,124 @@ void websitesWithTooManyDifferencesAreNotSuspicious() { assertFalse(isScamResult); } + @Test + @DisplayName("Messages containing multiple suspicious attachments are flagged as scam") + void detectsSuspiciousAttachments() { + // GIVEN an empty message containing suspicious attachments + String content = ""; + Message.Attachment attachment = createImageAttachmentMock(SUSPICIOUS_ATTACHMENT_NAME); + List attachments = + Collections.nCopies(SUSPICIOUS_ATTACHMENTS_THRESHOLD, attachment); + Message message = createMessageMock(content, attachments); + + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(message); + + // THEN flags it as scam + assertTrue(isScamResult); + } + + @Test + @DisplayName("Messages containing text content are not flagged for suspicious attachments") + void ignoresAttachmentsIfContentProvided() { + // GIVEN a non-empty message containing suspicious attachments + String content = "Hello World"; + Message.Attachment attachment = createImageAttachmentMock(SUSPICIOUS_ATTACHMENT_NAME); + List attachments = + Collections.nCopies(SUSPICIOUS_ATTACHMENTS_THRESHOLD, attachment); + Message message = createMessageMock(content, attachments); + + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(message); + + // THEN flags it as harmless + assertFalse(isScamResult); + } + + @Test + @DisplayName("Messages containing not enough suspicious attachments are not flagged") + void ignoresIfNotEnoughSuspiciousAttachments() { + // GIVEN an empty message containing some, but not enough suspicious attachments + String content = ""; + + Message.Attachment badAttachment = createImageAttachmentMock(SUSPICIOUS_ATTACHMENT_NAME); + Message.Attachment goodAttachment = createImageAttachmentMock("good.png"); + int badAttachmentAmount = SUSPICIOUS_ATTACHMENTS_THRESHOLD - 1; + List attachments = + new ArrayList<>(Collections.nCopies(badAttachmentAmount, badAttachment)); + attachments.add(goodAttachment); + + Message message = createMessageMock(content, attachments); + + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(message); + + // THEN flags it as harmless + assertFalse(isScamResult); + } + + @Test + @DisplayName("Messages containing harmless attachments are not flagged") + void ignoresHarmlessAttachments() { + // GIVEN an empty message containing only harmless attachments + String content = ""; + Message.Attachment attachment = createImageAttachmentMock("good.png"); + List attachments = + Collections.nCopies(SUSPICIOUS_ATTACHMENTS_THRESHOLD, attachment); + Message message = createMessageMock(content, attachments); + + // WHEN analyzing it + boolean isScamResult = scamDetector.isScam(message); + + // THEN flags it as harmless + 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); + when(message.getContentDisplay()).thenReturn(content); + when(message.getAttachments()).thenReturn(attachments); + return message; + } + + private static Message.Attachment createImageAttachmentMock(String name) { + Message.Attachment attachment = mock(Message.Attachment.class); + when(attachment.isImage()).thenReturn(true); + when(attachment.getFileName()).thenReturn(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""", @@ -237,14 +370,124 @@ private static List provideRealScamMessages() { Or via TG: https://t.me/Charlie_Adamo """, "Urgently looking for mods & collab managers https://discord.gg/cryptohireo", - "Check this - https://transfer.sh/get/ajmkh3l7tzop/Setup.exe"); + "Check this - https://transfer.sh/get/ajmkh3l7tzop/Setup.exe", + """ + Secrets of the crypto market that top traders don’t want you to know! I’m looking to help some individuals who + are serious about earning over $100K weekly in the market. Remember, I’ll require just 15% of your profits once + you start seeing earnings. Note: I’m only looking for serious and truly interested individuals. + Text me on TG/WhatApps for more info on how to get started +(123)123-1230 https://t.me/officialjohnsmith""", + """ + 💻 Senior Full Stack Engineer | 8+ Years Experience with me + Hi, I’m a Senior Software Engineer with over 8 years of experience building scalable website, cloud-native software solutions across industries like healthcare, fintech, e-commerce, gaming, logistics, and energy. + 🧰 Core Skills: + Frontend: React, Vue, Angular, Next.js, TypeScript, Web3 integration, Svelte, Three.js, Pixi.js + Backend: Node.js, NestJS, PHP (Laravel, Symfony), Python (FastAPI/Flask), .Net, Rails + Databases: MongoDB, MySQL, PostgreSQL, Redis + Ecommerce platforms: MedusaJS, MercurJS, Shopify (Gadget) + Automation & Bots: Token Swap / Trading Bots, AI/ML & Generative AI & CRM, Automation online sites + 🔍 Notable Projects: + Property Shield: Scalable backend with NestJS, Redis Streams, MongoDB, Supabase + Ready Education: Frontend state architecture with NgRx, Next / Vue, TypeScript with Web3, + Kozoom Multimedia: Secure enterprise login using React, Redux, Azure + B2CWorkflow Builder (React Flow) + 📂 Portfolio: https://tobimoller.space/ + 📬 Open to freelance gigs, contracts, and bounties — let’s talk!""", + """ + I'll help the first 10 people interested on how to start earning $100k or more within a week, + but you will reimburse me 10% of your profits when you receive it. Note: only interested people should + send a friend request or send me a dm! ask me (HOW) via Telegram username @JohnSmith_123""", + """ + Ready to unlock your earning potential in the digital market? you can start earning $100,000 and even more + as a beginner from the digital market, DM me for expert guidance or contact me directly on telegram and start building your financial future. + Telegram username @JohnSmith123""", + "Grab it before it's deleted (available for Windows and macOS): https://www.reddit.com/r/TVBaFreeHub/comments/12345t/ninaatradercrackedfullpowertradingfreefor123/"); } private static List provideRealFalsePositiveMessages() { - return List - .of(""" - https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types""", - """ - And according to quick google search. Median wage is about $23k usd"""); + return List.of( + """ + https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types""", + "And according to quick google search. Median wage is about $23k usd", + """ + $ docker image prune -a + WARNING! This will remove all images without at least one container associated to them. + Are you sure you want to continue? [y/N] y + ... + Total reclaimed space: 37.73GB""", + """ + Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.String org.junit.platform.engine.discovery.MethodSelector.getMethodParameterTypes()' + at com.intellij.junit5.JUnit5TestRunnerUtil.loadMethodByReflection(JUnit5TestRunnerUtil.java:127) + at com.intellij.junit5.JUnit5TestRunnerUtil.buildRequest(JUnit5TestRunnerUtil.java:102) + at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:43) + at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38) + at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11) + at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35) + at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232) + at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)""", + """ + The average wage here (not the median, which is lower) gives you a take-home of about $68k in New Zealand dollars. + The median house-price in my city (which is not at all the most expensive city) is ~$740k. + That's an 11 year save for an average earner for an average house without spending anything.""", + "https://thehackernews.com/2025/07/alert-exposed-jdwp-interfaces-lead-to.html", + """ + ~/Developer/TJ-Bot develop ❯ ./gradlew build 10:20:05 PM + FAILURE: Build failed with an exception. + What went wrong: + class name.remal.gradleplugins.sonarlint.SonarLintPlugin + tried to access private field org.gradle.api.plugins.quality.internal.AbstractCodeQualityPlugin.extension + (name.remal.gradleplugins.sonarlint.SonarLintPlugin is in unnamed module of loader + org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @55f4c79b; + org.gradle.api.plugins.quality.internal.AbstractCodeQualityPlugin is in unnamed module of + loader org.gradle.initialization.MixInLegacyTypesClassLoader @49b2a47d) + Try: + Run with --stacktrace option to get the stack trace. + Run with --info or --debug option to get more log output. + Run with --scan to get full insights. + Get more help at https://help.gradle.org/. + BUILD FAILED in 795ms + 7 actionable tasks: 7 up-to-date + ~/Developer/TJ-Bot develop ❯""", + """ + For example. I enter 3.45 for the price and 3 for the count. It results in 10.350000000000001 for some reason. I followed Bro Code's video: + https://www.youtube.com/watch?v=P8CVPIaRmys&list=PLZPZq0rRZOOjNOZYq_R2PECIMglLemc&index=6 + and his does not do this. Why is this? + import java.util.Scanner; + public class ShoppingCart { + public static void main(String[] args){ + // Shopping Cart Arithmetic Practice + Scanner input = new Scanner(System.in); + String item; + double price; + int count; + char currency = '$'; + double total; + System.out.print("What item would you like to buy?: "); + item = input.nextLine(); + System.out.print("What is the price of this item?: "); + price = input.nextDouble(); + System.out.print("How many " + item + "(s) would you like to buy?: "); + count = input.nextInt(); + total = price * count; + System.out.println("\\nYou bought " + count + " " + item + "(s).\\n"); + System.out.println("Your total is " + currency + total); + } + }""", + "@squidxtv https://cdn.steamusercontent.com/ugc/12827361819537692968/A7B3AC5A176E7B2287B5E84B9A0BE9754F5A6388/", + """ + today i understood, why security is joke, even for people on top + https://micahsmith.com/ddosecrets-publishes-410-gb-of-heap-dumps-hacked-from-telemessages-archive-server/""", + """ + Hey guys @everyone, apologise for disturbing, + I wanted to ask what's the scope of Java in future like after 2030 in USA, like the newer frameworks will + replace Spring Boot ... and how AI will play it role ... + I am very much confused, what to do, I tired exploring Machine Learning, but I don't know why it felt more + like a burden then enjoyment, but spring boot was fun, although exploring microservice architecture + is was tricky mostly when it came to deployment and it become really confusing...""", + "https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/", + """ + It was pretty pricey, and the costs likely differ a lot from country to country + (keeping in mind that a portion is importing of equipment to NZ and some is labour in a very different market). + We have 13.5KW of storage, a 10KW inverter, 11.5KW of generation and an EV charger. + All up, on a 1% 'green loan', it was $40k NZD (~$23k USD)"""); } } diff --git a/build.gradle b/build.gradle index d0588c0496..da03a76d96 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id "com.diffplug.spotless" version "7.0.4" + id "com.diffplug.spotless" version "7.2.0" id "org.sonarqube" version "6.2.0.5505" id "name.remal.sonarlint" version "5.1.10" }