Reddit AI BotBuster

Detects suspected bot and AI-generated posts/comments on Reddit using advanced heuristics. Highlights flagged elements, shows a styled popup, and lists clickable bot/AI accounts in the dropdown.

Od 08.03.2025.. Pogledajte najnovija verzija.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Reddit AI BotBuster
// @namespace    http://tampermonkey.net/
// @version      2.8
// @description  Detects suspected bot and AI-generated posts/comments on Reddit using advanced heuristics. Highlights flagged elements, shows a styled popup, and lists clickable bot/AI accounts in the dropdown.
// @match        https://www.reddit.com/*
// @match        https://old.reddit.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    /***********************
     * 1. Inject CSS for Bot Username Styling
     ***********************/
    const style = document.createElement("style");
    style.innerHTML = `
        .botUsername {
            color: orange !important;
            font-size: 14px !important;
        }
        /* Smooth scrolling for anchor jumps */
        html {
            scroll-behavior: smooth;
        }
    `;
    document.head.appendChild(style);

    /***********************
     * CONFIGURATION
     ***********************/
    const RED_FLAG_THRESHOLD = 5;  // Combined score threshold

    // Bot-related heuristics
    const suspiciousUserPatterns = [
        /bot/i,
        /^[A-Za-z]+-[A-Za-z]+\d{4}$/,
        /^[A-Za-z]+[A-Za-z]+\d+$/,
        /^[A-Z][a-z]+[A-Z][a-z]+s{2,}$/
    ];

    function isRandomString(username) {
        if (username.length < 8) return false;
        const vowels = username.match(/[aeiou]/gi);
        const ratio = vowels ? vowels.length / username.length : 0;
        return ratio < 0.3;
    }

    const genericResponses = [
        "i agree dude",
        "yes you are right",
        "well said",
        "totally agree",
        "i agree",
        "right you are",
        "well spoken, you are",
        "perfectly said this is"
    ];

    const scamLinkRegex = /\.(live|life|shop)\b/i;

    function isNewAccount(userElem) {
        const titleAttr = userElem.getAttribute('title') || "";
        return /redditor for.*\b(day|week|month)\b/i.test(titleAttr);
    }

    // Parse account age in months from user tooltip
    function getAccountAge(userElem) {
        const titleAttr = userElem.getAttribute('title') || "";
        const match = titleAttr.match(/redditor for (\d+)\s*(day|week|month|year)s?/i);
        if (match) {
            let value = parseInt(match[1]);
            let unit = match[2].toLowerCase();
            if (unit === "day") return value / 30;
            if (unit === "week") return (value * 7) / 30;
            if (unit === "month") return value;
            if (unit === "year") return value * 12;
        }
        return null;
    }

    // Track duplicates
    const commentTextMap = new Map();

    /***********************
     * AI DETECTION HEURISTICS
     * (Slightly lowered weights to reduce false positives)
     ***********************/
    function computeAIScore(text) {
        let score = 0;
        let lowerText = text.toLowerCase();

        // 1. AI disclaimers (slightly reduced)
        if (lowerText.includes("as an ai language model") || lowerText.includes("i am not a human")) {
            score += 1.9; // from 2.0
        }

        // 2. Lack of contractions in long texts (slightly reduced)
        let contractions = lowerText.match(/\b(i'm|you're|they're|we're|can't|won't|didn't|isn't|aren't)\b/g);
        let words = lowerText.split(/\s+/);
        let wordCount = words.length;
        if (wordCount > 150 && (!contractions || contractions.length === 0)) {
            score += 1.2; // from 1.3
        }

        // 3. Formal filler phrases
        const aiPhrases = ["in conclusion", "furthermore", "moreover", "on the other hand"];
        aiPhrases.forEach(phrase => {
            if (lowerText.includes(phrase)) {
                score += 0.5;
            }
        });

        // 4. Common AI indicator phrases (slightly reduced)
        const aiIndicators = [
            "i do not have personal experiences",
            "my training data",
            "i cannot",
            "i do not have the ability",
            "apologies if",
            "i apologize",
            "i'm unable",
            "as an ai",
            "as an artificial intelligence"
        ];
        aiIndicators.forEach(phrase => {
            if (lowerText.includes(phrase)) {
                score += 0.9; // from 1.0
            }
        });

        // 5. Repetitiveness / low burstiness (slightly reduced)
        let sentences = lowerText.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
        if (sentences.length > 1) {
            let lengths = sentences.map(s => s.split(/\s+/).length);
            let avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
            let variance = lengths.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / lengths.length;
            if (variance < 4) {
                score += 0.9; // from 1.0
            }
        }

        // 6. Lexical diversity (slightly reduced)
        let uniqueWords = new Set(words);
        let typeTokenRatio = uniqueWords.size / words.length;
        if (words.length > 20 && typeTokenRatio < 0.3) {
            score += 0.9; // from 1.0
        }

        // 7. Bigram uniqueness (slightly reduced)
        function getBigrams(arr) {
            let bigrams = [];
            for (let i = 0; i < arr.length - 1; i++) {
                bigrams.push(arr[i] + " " + arr[i+1]);
            }
            return bigrams;
        }
        let bigrams = getBigrams(words);
        if (bigrams.length > 0) {
            let uniqueBigrams = new Set(bigrams);
            let bigramRatio = uniqueBigrams.size / bigrams.length;
            if (bigramRatio < 0.5) {
                score += 0.9; // from 1.0
            }
        }

        // 8. Trigram uniqueness (slightly reduced)
        function getTrigrams(arr) {
            let trigrams = [];
            for (let i = 0; i < arr.length - 2; i++) {
                trigrams.push(arr[i] + " " + arr[i+1] + " " + arr[i+2]);
            }
            return trigrams;
        }
        let trigrams = getTrigrams(words);
        if (trigrams.length > 0) {
            let uniqueTrigrams = new Set(trigrams);
            let trigramRatio = uniqueTrigrams.size / trigrams.length;
            if (trigramRatio < 0.6) {
                score += 0.7; // from 0.8
            }
        }

        // Enhancements
        score += computeSemanticCoherenceScore(text); // 0.7
        score += computeProperNounConsistencyScore(text); // 0.4
        score += computeContextShiftScore(text); // 0.7
        score += computeSyntaxScore(text); // 1.0

        return score;
    }

    function computeSemanticCoherenceScore(text) {
        let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
        if (sentences.length < 2) return 0;
        let similarities = [];
        for (let i = 0; i < sentences.length - 1; i++) {
            let s1 = new Set(sentences[i].toLowerCase().split(/\s+/));
            let s2 = new Set(sentences[i+1].toLowerCase().split(/\s+/));
            let intersection = new Set([...s1].filter(x => s2.has(x)));
            let union = new Set([...s1, ...s2]);
            let jaccard = union.size ? intersection.size / union.size : 0;
            similarities.push(jaccard);
        }
        let avgSim = similarities.reduce((a, b) => a + b, 0) / similarities.length;
        return avgSim < 0.2 ? 0.7 : 0;
    }

    function computeProperNounConsistencyScore(text) {
        let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
        if (sentences.length < 2) return 0;
        let counts = sentences.map(sentence => {
            let words = sentence.split(/\s+/);
            let properNouns = words.slice(1).filter(word => /^[A-Z][a-z]+/.test(word));
            return properNouns.length;
        });
        let avg = counts.reduce((a, b) => a + b, 0) / counts.length;
        let variance = counts.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / counts.length;
        return variance > 2 ? 0.4 : 0;
    }

    function computeContextShiftScore(text) {
        let words = text.split(/\s+/).filter(w => w.length > 0);
        if (words.length < 10) return 0;
        let half = Math.floor(words.length / 2);
        let firstHalf = words.slice(0, half);
        let secondHalf = words.slice(half);
        let freq = arr => {
            let f = {};
            arr.forEach(word => { f[word] = (f[word] || 0) + 1; });
            return f;
        };
        let f1 = freq(firstHalf), f2 = freq(secondHalf);
        let allWords = new Set([...Object.keys(f1), ...Object.keys(f2)]);
        let dot = 0, norm1 = 0, norm2 = 0;
        allWords.forEach(word => {
            let v1 = f1[word] || 0;
            let v2 = f2[word] || 0;
            dot += v1 * v2;
            norm1 += v1 * v1;
            norm2 += v2 * v2;
        });
        let cosSim = (norm1 && norm2) ? dot / (Math.sqrt(norm1) * Math.sqrt(norm2)) : 0;
        return cosSim < 0.3 ? 0.7 : 0;
    }

    function computeSyntaxScore(text) {
        let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
        if (sentences.length === 0) return 0;
        let punctuationMatches = text.match(/[,\;:\-]/g);
        let punctuationCount = punctuationMatches ? punctuationMatches.length : 0;
        let avgPunctuation = punctuationCount / sentences.length;
        return avgPunctuation < 1 ? 1.0 : 0;
    }

    /***********************
     * BOT SCORE
     ***********************/
    function computeBotScore(elem) {
        let score = 0;
        // Attempt multiple possible selectors for user links
        const userElem = elem.querySelector('a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]');
        if (userElem) {
            const username = userElem.innerText.trim();
            if (username) {
                if (username.toLowerCase().includes("bot")) {
                    score++;
                }
                suspiciousUserPatterns.forEach(pattern => {
                    if (pattern.test(username)) {
                        score++;
                    }
                });
                if (isRandomString(username)) {
                    score++;
                }
            }
            if (isNewAccount(userElem)) {
                score++;
            }
            let ageInMonths = getAccountAge(userElem);
            if (ageInMonths !== null && ageInMonths < 2) {
                score += 0.5;
            }
        }
        let textContent = elem.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
        genericResponses.forEach(phrase => {
            if (textContent === phrase) {
                score++;
            }
        });
        if (textContent.split(' ').length < 3) {
            score++;
        }
        if (textContent.includes("&amp;") && !textContent.includes("& ")) {
            score++;
        }
        if (textContent.startsWith('>') && textContent.split(' ').length < 5) {
            score++;
        }
        if (textContent.length > 0) {
            const count = commentTextMap.get(textContent) || 0;
            if (count > 0) {
                score++;
            }
        }
        const links = elem.querySelectorAll('a');
        links.forEach(link => {
            if (scamLinkRegex.test(link.href)) {
                score++;
            }
            if (link.parentElement && link.parentElement.innerText.includes("Powered by Gearlaunch")) {
                score++;
            }
        });
        return score;
    }

    function countRedFlags(elem) {
        const botScore = computeBotScore(elem);
        const aiScore = computeAIScore(elem.innerText);
        return { botScore, aiScore, totalScore: botScore + aiScore };
    }

    /***********************
     * DETECTIONS & POPUP
     ***********************/
    let botCount = 0;
    let detectedBots = [];
    let detectionIndex = 0; // For anchor ID assignment

    function createPopup() {
        let popup = document.getElementById("botCounterPopup");
        if (!popup) {
            popup = document.createElement("div");
            popup.id = "botCounterPopup";
            popup.style.position = "fixed";
            popup.style.top = "40px";
            popup.style.right = "10px";
            popup.style.backgroundColor = "rgba(248,248,248,0.5)";
            popup.style.border = "1px solid #ccc";
            popup.style.padding = "10px";
            popup.style.zIndex = "9999";
            popup.style.fontFamily = "Roboto, sans-serif";
            popup.style.fontSize = "12px";
            popup.style.cursor = "pointer";
            popup.style.backdropFilter = "blur(5px)";
            popup.style.webkitBackdropFilter = "blur(5px)";
            popup.style.width = "250px";
            let header = document.createElement("div");
            header.id = "botPopupHeader";
            header.innerText = "Detected bot/AI content: " + botCount;
            if (botCount < 10) {
                header.style.color = "green";
            } else if (botCount < 30) {
                header.style.color = "yellow";
            } else {
                header.style.color = "red";
            }
            popup.appendChild(header);
            let dropdown = document.createElement("div");
            dropdown.id = "botDropdown";
            dropdown.style.display = "none";
            dropdown.style.maxHeight = "300px";
            dropdown.style.overflowY = "auto";
            dropdown.style.marginTop = "10px";
            dropdown.style.borderTop = "1px solid #ccc";
            dropdown.style.paddingTop = "5px";
            popup.appendChild(dropdown);

            popup.addEventListener("click", function(e) {
                e.stopPropagation();
                dropdown.style.display = (dropdown.style.display === "none") ? "block" : "none";
            });

            document.body.appendChild(popup);
        }
    }

    function updatePopup() {
        let header = document.getElementById("botPopupHeader");
        let dropdown = document.getElementById("botDropdown");
        if (header) {
            header.innerText = "Detected bot/AI content: " + botCount;
            if (botCount < 10) {
                header.style.color = "green";
            } else if (botCount < 30) {
                header.style.color = "yellow";
            } else {
                header.style.color = "red";
            }
        }
        if (dropdown) {
            dropdown.innerHTML = "";
            if (detectedBots.length === 0) {
                let emptyMsg = document.createElement("div");
                emptyMsg.innerText = "No bots/AI detected.";
                dropdown.appendChild(emptyMsg);
            } else {
                detectedBots.forEach(function(item) {
                    let entry = document.createElement("div");
                    entry.style.marginBottom = "5px";

                    // Link to jump to the flagged comment
                    let link = document.createElement("a");
                    link.href = "#" + item.elemID; // anchor link
                    link.style.color = "inherit";
                    link.style.textDecoration = "none";
                    link.style.cursor = "pointer";
                    link.innerText = item.username;

                    entry.appendChild(link);
                    dropdown.appendChild(entry);
                });
            }
        }
    }

    createPopup();
    updatePopup();

    function highlightIfSuspected(elem) {
        if (elem.getAttribute("data-bot-detected") === "true") return;
        const scores = countRedFlags(elem);
        if (scores.totalScore >= RED_FLAG_THRESHOLD) {
            let reason = "";
            if (scores.botScore > 0 && scores.aiScore > 0) {
                reason = "Bot/AI";
            } else if (scores.aiScore > 0) {
                reason = "AI";
            } else {
                reason = "Bot";
            }

            // Outline
            if (reason === "Bot") {
                elem.style.outline = "3px dashed red";
            } else if (reason === "AI") {
                elem.style.outline = "3px dashed blue";
            } else {
                elem.style.outline = "3px dashed purple";
            }
            elem.setAttribute("data-bot-detected", "true");
            botCount++;

            // Assign an ID so we can anchor to it
            detectionIndex++;
            const elemID = "botbuster-detected-" + detectionIndex;
            elem.setAttribute("id", elemID);

            // If reason includes "bot", style the username
            if (reason.toLowerCase().includes("bot")) {
                const userLinks = elem.querySelectorAll(
                  'a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]'
                );
                userLinks.forEach(link => {
                    link.classList.add("botUsername");
                });
            }

            // Record
            if (!elem.getAttribute("data-bot-recorded")) {
                let userElemForRecord = elem.querySelector(
                  'a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]'
                );
                let username = userElemForRecord ? userElemForRecord.innerText.trim() : "Unknown";
                // We only store the username and an anchor ID
                detectedBots.push({
                    username: username,
                    elemID: elemID
                });
                elem.setAttribute("data-bot-recorded", "true");
            }
            updatePopup();
            console.log("BotBuster: Highlighted element with", scores.totalScore, "red flags. Reason:", reason);
        }
    }

    function scanForBots(root = document) {
        const selectors = [
            'div[data-testid="post-container"]',
            'div[data-testid="comment"]',
            'div.thing',
            'div.Comment'
        ];
        const candidates = root.querySelectorAll(selectors.join(', '));
        candidates.forEach(candidate => {
            let textContent = candidate.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
            if (textContent.length > 0) {
                const currentCount = commentTextMap.get(textContent) || 0;
                commentTextMap.set(textContent, currentCount + 1);
            }
            highlightIfSuspected(candidate);
        });
    }

    /***********************
     * OBSERVATION
     ***********************/
    scanForBots();

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    scanForBots(node);
                }
            });
        });
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setInterval(() => {
        scanForBots(document);
    }, 3000);

})();