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