- // ==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("&") && !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);
-
- })();