Reddit AI BotBuster

Detects suspected bot accounts and AI-generated content on Reddit using advanced heuristics. Bot accounts have their username styled in orange (if the account is young), while AI-generated content is outlined.

  1. // ==UserScript==
  2. // @name Reddit AI BotBuster
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.10.3
  5. // @description Detects suspected bot accounts and AI-generated content on Reddit using advanced heuristics. Bot accounts have their username styled in orange (if the account is young), while AI-generated content is outlined.
  6. // @match https://www.reddit.com/*
  7. // @match https://old.reddit.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. /***********************
  15. * 1. Inject CSS for Bot Username Styling
  16. ***********************/
  17. const style = document.createElement("style");
  18. style.innerHTML = `
  19. .botUsername {
  20. color: orange !important;
  21. font-size: 14px !important;
  22. }
  23. html {
  24. scroll-behavior: smooth;
  25. }
  26. `;
  27. document.head.appendChild(style);
  28.  
  29. /***********************
  30. * CONFIGURATION
  31. ***********************/
  32. // Thresholds for flagging:
  33. const BOT_THRESHOLD = 2; // For bot username detection.
  34. const AI_THRESHOLD = 3; // For AI-generated content detection.
  35.  
  36. // --- Bot-related heuristics patterns ---
  37. const suspiciousUserPatterns = [
  38. /bot/i,
  39. /^[A-Za-z]+-[A-Za-z]+\d{4}$/,
  40. /^[A-Za-z]+[A-Za-z]+\d+$/,
  41. /^[A-Z][a-z]+[A-Z][a-z]+s{2,}$/
  42. ];
  43.  
  44. // New, conservative username scoring.
  45. function computeUsernameBotScore(username) {
  46. let score = 0;
  47. if (username.toLowerCase().includes("bot")) { score += 0.5; }
  48. suspiciousUserPatterns.forEach(pattern => {
  49. if (pattern.test(username)) { score += 0.5; }
  50. });
  51. let vowels = username.match(/[aeiou]/gi);
  52. let vowelRatio = vowels ? vowels.length / username.length : 0;
  53. if (vowelRatio < 0.3) { score += 0.5; }
  54. let digits = username.match(/\d/g);
  55. if (digits && (digits.length / username.length) > 0.5) { score += 0.5; }
  56. if (username.length < 4 || username.length > 20) { score += 0.5; }
  57. return score;
  58. }
  59.  
  60. function isRandomString(username) {
  61. if (username.length < 8) return false;
  62. const vowels = username.match(/[aeiou]/gi);
  63. const ratio = vowels ? vowels.length / username.length : 0;
  64. return ratio < 0.3;
  65. }
  66.  
  67. const genericResponses = [
  68. "i agree dude",
  69. "yes you are right",
  70. "well said",
  71. "totally agree",
  72. "i agree",
  73. "right you are",
  74. "well spoken, you are",
  75. "perfectly said this is"
  76. ];
  77.  
  78. const scamLinkRegex = /\.(live|life|shop)\b/i;
  79.  
  80. function isNewAccount(userElem) {
  81. const titleAttr = userElem.getAttribute('title') || "";
  82. return /redditor for.*\b(day|week|month)\b/i.test(titleAttr);
  83. }
  84.  
  85. // Parse account age in months from user tooltip.
  86. function getAccountAge(userElem) {
  87. const titleAttr = userElem.getAttribute('title') || "";
  88. const match = titleAttr.match(/redditor for (\d+)\s*(day|week|month|year)s?/i);
  89. if (match) {
  90. let value = parseInt(match[1]);
  91. let unit = match[2].toLowerCase();
  92. if (unit === "day") return value / 30;
  93. if (unit === "week") return (value * 7) / 30;
  94. if (unit === "month") return value;
  95. if (unit === "year") return value * 12;
  96. }
  97. return null;
  98. }
  99.  
  100. // Global map to track duplicate comment texts.
  101. const commentTextMap = new Map();
  102.  
  103. /***********************
  104. * READABILITY HEURISTIC
  105. ***********************/
  106. function countSyllables(word) {
  107. word = word.toLowerCase();
  108. if (word.length <= 3) { return 1; }
  109. word = word.replace(/e\b/g, "");
  110. let matches = word.match(/[aeiouy]{1,}/g);
  111. return matches ? matches.length : 1;
  112. }
  113.  
  114. function computeReadabilityScore(text) {
  115. let sentenceMatches = text.match(/[^.!?]+[.!?]+/g);
  116. if (!sentenceMatches) return null;
  117. let sentences = sentenceMatches;
  118. let words = text.split(/\s+/).filter(w => w.length > 0);
  119. let sentenceCount = sentences.length;
  120. let wordCount = words.length;
  121. let syllableCount = 0;
  122. words.forEach(word => { syllableCount += countSyllables(word); });
  123. let flesch = 206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (syllableCount / wordCount);
  124. return flesch;
  125. }
  126.  
  127. /***********************
  128. * ADVANCED AI DETECTION HEURISTICS
  129. ***********************/
  130. function computeAIScore(text) {
  131. let score = 0;
  132. let lowerText = text.toLowerCase();
  133.  
  134. if (lowerText.includes("as an ai language model") || lowerText.includes("i am not a human")) {
  135. score += 1.8;
  136. }
  137.  
  138. 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);
  139. let words = lowerText.split(/\s+/);
  140. let wordCount = words.length;
  141. if (wordCount > 150 && (!contractions || contractions.length === 0)) {
  142. score += 1.2;
  143. }
  144.  
  145. const aiPhrases = ["in conclusion", "furthermore", "moreover", "on the other hand"];
  146. aiPhrases.forEach(phrase => { if (lowerText.includes(phrase)) { score += 0.5; } });
  147.  
  148. const aiIndicators = [
  149. "i do not have personal experiences",
  150. "my training data",
  151. "i cannot",
  152. "i do not have the ability",
  153. "apologies if",
  154. "i apologize",
  155. "i'm unable",
  156. "as an ai",
  157. "as an artificial intelligence"
  158. ];
  159. aiIndicators.forEach(phrase => { if (lowerText.includes(phrase)) { score += 1.0; } });
  160.  
  161. let sentencesArr = lowerText.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
  162. if (sentencesArr.length > 1) {
  163. let lengths = sentencesArr.map(s => s.split(/\s+/).length);
  164. let avg = lengths.reduce((a, b) => a + b, 0) / lengths.length;
  165. let variance = lengths.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / lengths.length;
  166. if (variance < 4) { score += 0.8; }
  167. }
  168.  
  169. let uniqueWords = new Set(words);
  170. let typeTokenRatio = uniqueWords.size / words.length;
  171. if (words.length > 20 && typeTokenRatio < 0.3) { score += 1.0; }
  172.  
  173. function getBigrams(arr) {
  174. let bigrams = [];
  175. for (let i = 0; i < arr.length - 1; i++) {
  176. bigrams.push(arr[i] + " " + arr[i+1]);
  177. }
  178. return bigrams;
  179. }
  180. let bigrams = getBigrams(words);
  181. if (bigrams.length > 0) {
  182. let uniqueBigrams = new Set(bigrams);
  183. let bigramRatio = uniqueBigrams.size / bigrams.length;
  184. if (bigramRatio < 0.5) { score += 0.8; }
  185. }
  186.  
  187. function getTrigrams(arr) {
  188. let trigrams = [];
  189. for (let i = 0; i < arr.length - 2; i++) {
  190. trigrams.push(arr[i] + " " + arr[i+1] + " " + arr[i+2]);
  191. }
  192. return trigrams;
  193. }
  194. let trigrams = getTrigrams(words);
  195. if (trigrams.length > 0) {
  196. let uniqueTrigrams = new Set(trigrams);
  197. let trigramRatio = uniqueTrigrams.size / trigrams.length;
  198. if (trigramRatio < 0.6) { score += 0.8; }
  199. }
  200.  
  201. score += computeSemanticCoherenceScore(text); // now returns 0.6
  202. score += computeProperNounConsistencyScore(text); // now returns 0.3
  203. score += computeContextShiftScore(text); // now returns 0.6
  204. score += computeSyntaxScore(text); // now returns 0.9
  205.  
  206. let readability = computeReadabilityScore(text);
  207. if (readability !== null && readability > 80) { score += 0.55; }
  208.  
  209. return score;
  210. }
  211.  
  212. function computeSemanticCoherenceScore(text) {
  213. let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
  214. if (sentences.length < 2) return 0;
  215. let similarities = [];
  216. for (let i = 0; i < sentences.length - 1; i++) {
  217. let s1 = new Set(sentences[i].toLowerCase().split(/\s+/));
  218. let s2 = new Set(sentences[i+1].toLowerCase().split(/\s+/));
  219. let intersection = new Set([...s1].filter(x => s2.has(x)));
  220. let union = new Set([...s1, ...s2]);
  221. let jaccard = union.size ? intersection.size / union.size : 0;
  222. similarities.push(jaccard);
  223. }
  224. let avgSim = similarities.reduce((a, b) => a + b, 0) / similarities.length;
  225. return avgSim < 0.2 ? 0.6 : 0;
  226. }
  227.  
  228. function computeProperNounConsistencyScore(text) {
  229. let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
  230. if (sentences.length < 2) return 0;
  231. let counts = sentences.map(sentence => {
  232. let words = sentence.split(/\s+/);
  233. let properNouns = words.slice(1).filter(word => /^[A-Z][a-z]+/.test(word));
  234. return properNouns.length;
  235. });
  236. let avg = counts.reduce((a, b) => a + b, 0) / counts.length;
  237. let variance = counts.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / counts.length;
  238. return variance > 2 ? 0.3 : 0;
  239. }
  240.  
  241. function computeContextShiftScore(text) {
  242. let words = text.split(/\s+/).filter(w => w.length > 0);
  243. if (words.length < 10) return 0;
  244. let half = Math.floor(words.length / 2);
  245. let firstHalf = words.slice(0, half);
  246. let secondHalf = words.slice(half);
  247. let freq = arr => {
  248. let f = {};
  249. arr.forEach(word => { f[word] = (f[word] || 0) + 1; });
  250. return f;
  251. };
  252. let f1 = freq(firstHalf), f2 = freq(secondHalf);
  253. let allWords = new Set([...Object.keys(f1), ...Object.keys(f2)]);
  254. let dot = 0, norm1 = 0, norm2 = 0;
  255. allWords.forEach(word => {
  256. let v1 = f1[word] || 0;
  257. let v2 = f2[word] || 0;
  258. dot += v1 * v2;
  259. norm1 += v1 * v1;
  260. norm2 += v2 * v2;
  261. });
  262. let cosSim = (norm1 && norm2) ? dot / (Math.sqrt(norm1) * Math.sqrt(norm2)) : 0;
  263. return cosSim < 0.3 ? 0.6 : 0;
  264. }
  265.  
  266. function computeSyntaxScore(text) {
  267. let sentences = text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 0);
  268. if (sentences.length === 0) return 0;
  269. let punctuationMatches = text.match(/[,\;:\-]/g);
  270. let punctuationCount = punctuationMatches ? punctuationMatches.length : 0;
  271. let avgPunctuation = punctuationCount / sentences.length;
  272. return avgPunctuation < 1 ? 0.9 : 0;
  273. }
  274.  
  275. /***********************
  276. * BOT SCORE CALCULATION
  277. ***********************/
  278. function computeBotScore(elem) {
  279. let score = 0;
  280. const userElem = elem.querySelector('a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]');
  281. if (userElem) {
  282. const username = userElem.innerText.trim();
  283. let ageInMonths = getAccountAge(userElem);
  284. // Only add username-based bot signals if account age is 60 months or less.
  285. if (ageInMonths === null || ageInMonths <= 60) {
  286. if (username) { score += computeUsernameBotScore(username); }
  287. if (isNewAccount(userElem)) { score += 2; }
  288. if (ageInMonths !== null && ageInMonths < 2) { score += 1; }
  289. }
  290. }
  291. let textContent = elem.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
  292. genericResponses.forEach(phrase => { if (textContent === phrase) { score++; } });
  293. if (textContent.split(' ').length < 3) { score++; }
  294. if (textContent.includes("&amp;") && !textContent.includes("& ")) { score++; }
  295. if (textContent.startsWith('>') && textContent.split(' ').length < 5) { score++; }
  296. if (textContent.length > 0) {
  297. const count = commentTextMap.get(textContent) || 0;
  298. if (count > 0) { score++; }
  299. }
  300. const links = elem.querySelectorAll('a');
  301. links.forEach(link => {
  302. if (scamLinkRegex.test(link.href)) { score++; }
  303. if (link.parentElement && link.parentElement.innerText.includes("Powered by Gearlaunch")) { score++; }
  304. });
  305. return score;
  306. }
  307.  
  308. function countRedFlags(elem) {
  309. const botScore = computeBotScore(elem);
  310. const aiScore = computeAIScore(elem.innerText);
  311. return { botScore, aiScore, totalScore: botScore + aiScore };
  312. }
  313.  
  314. /***********************
  315. * DETECTIONS & POPUP
  316. ***********************/
  317. let botCount = 0;
  318. let detectedBots = [];
  319. let detectionIndex = 0;
  320.  
  321. function createPopup() {
  322. let popup = document.getElementById("botCounterPopup");
  323. if (!popup) {
  324. popup = document.createElement("div");
  325. popup.id = "botCounterPopup";
  326. popup.style.position = "fixed";
  327. popup.style.top = "40px";
  328. popup.style.right = "10px";
  329. popup.style.backgroundColor = "rgba(248,248,248,0.5)";
  330. popup.style.border = "1px solid #ccc";
  331. popup.style.padding = "10px";
  332. popup.style.zIndex = "9999";
  333. popup.style.fontFamily = "Roboto, sans-serif";
  334. popup.style.fontSize = "12px";
  335. popup.style.cursor = "pointer";
  336. popup.style.backdropFilter = "blur(5px)";
  337. popup.style.webkitBackdropFilter = "blur(5px)";
  338. popup.style.width = "250px";
  339. let header = document.createElement("div");
  340. header.id = "botPopupHeader";
  341. header.innerText = "Detected bot/AI content: " + botCount;
  342. if (botCount < 10) { header.style.color = "green"; }
  343. else if (botCount < 30) { header.style.color = "yellow"; }
  344. else { header.style.color = "red"; }
  345. popup.appendChild(header);
  346. let dropdown = document.createElement("div");
  347. dropdown.id = "botDropdown";
  348. dropdown.style.display = "none";
  349. dropdown.style.maxHeight = "300px";
  350. dropdown.style.overflowY = "auto";
  351. dropdown.style.marginTop = "10px";
  352. dropdown.style.borderTop = "1px solid #ccc";
  353. dropdown.style.paddingTop = "5px";
  354. popup.appendChild(dropdown);
  355. popup.addEventListener("click", function(e) {
  356. e.stopPropagation();
  357. dropdown.style.display = (dropdown.style.display === "none") ? "block" : "none";
  358. });
  359. document.body.appendChild(popup);
  360. }
  361. }
  362.  
  363. function updatePopup() {
  364. let header = document.getElementById("botPopupHeader");
  365. let dropdown = document.getElementById("botDropdown");
  366. if (header) {
  367. header.innerText = "Detected bot/AI content: " + botCount;
  368. if (botCount < 10) { header.style.color = "green"; }
  369. else if (botCount < 30) { header.style.color = "yellow"; }
  370. else { header.style.color = "red"; }
  371. }
  372. if (dropdown) {
  373. dropdown.innerHTML = "";
  374. if (detectedBots.length === 0) {
  375. let emptyMsg = document.createElement("div");
  376. emptyMsg.innerText = "No bots/AI detected.";
  377. dropdown.appendChild(emptyMsg);
  378. } else {
  379. detectedBots.forEach(function(item) {
  380. let entry = document.createElement("div");
  381. entry.style.marginBottom = "5px";
  382. let link = document.createElement("a");
  383. link.href = "#" + item.elemID;
  384. link.style.color = "inherit";
  385. link.style.textDecoration = "none";
  386. link.style.cursor = "pointer";
  387. link.innerText = item.username;
  388. entry.appendChild(link);
  389. dropdown.appendChild(entry);
  390. });
  391. }
  392. }
  393. }
  394.  
  395. createPopup();
  396. updatePopup();
  397.  
  398. function highlightIfSuspected(elem) {
  399. if (elem.getAttribute("data-bot-detected") === "true") return;
  400. const flags = countRedFlags(elem);
  401. const botFlag = flags.botScore >= BOT_THRESHOLD;
  402. const aiFlag = flags.aiScore >= AI_THRESHOLD;
  403. if (botFlag || aiFlag) {
  404. let reason = "";
  405. if (botFlag && aiFlag) { reason = "Bot & AI"; }
  406. else if (aiFlag) { reason = "AI"; }
  407. else { reason = "Bot"; }
  408. // Outline the element only if AI content is detected.
  409. if (aiFlag) {
  410. elem.style.outline = botFlag ? "3px dashed purple" : "3px dashed blue";
  411. }
  412. elem.setAttribute("data-bot-detected", "true");
  413. botCount++;
  414. detectionIndex++;
  415. const elemID = "botbuster-detected-" + detectionIndex;
  416. elem.setAttribute("id", elemID);
  417. // If the bot condition is met, style the username—unless account age > 60.
  418. if (botFlag) {
  419. const userElem = elem.querySelector('a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]');
  420. if (userElem) {
  421. let age = getAccountAge(userElem);
  422. if (age === null || age <= 60) {
  423. userElem.classList.add("botUsername");
  424. }
  425. }
  426. }
  427. if (!elem.getAttribute("data-bot-recorded")) {
  428. let userElemForRecord = elem.querySelector('a[href*="/user/"], a[href*="/u/"], a.author, a[data-click-id="user"]');
  429. let username = userElemForRecord ? userElemForRecord.innerText.trim() : "Unknown";
  430. detectedBots.push({ username: username, elemID: elemID });
  431. elem.setAttribute("data-bot-recorded", "true");
  432. }
  433. updatePopup();
  434. console.log("BotBuster: Flagged element. BotScore:", flags.botScore, "AI Score:", flags.aiScore);
  435. }
  436. }
  437.  
  438. function scanForBots(root = document) {
  439. const selectors = [
  440. 'div[data-testid="post-container"]',
  441. 'div[data-testid="comment"]',
  442. 'div.thing',
  443. 'div.Comment'
  444. ];
  445. const candidates = root.querySelectorAll(selectors.join(', '));
  446. candidates.forEach(candidate => {
  447. let textContent = candidate.innerText.toLowerCase().replace(/\s+/g, ' ').trim();
  448. if (textContent.length > 0) {
  449. const currentCount = commentTextMap.get(textContent) || 0;
  450. commentTextMap.set(textContent, currentCount + 1);
  451. }
  452. highlightIfSuspected(candidate);
  453. });
  454. }
  455.  
  456. /***********************
  457. * INITIALIZATION & OBSERVATION
  458. ***********************/
  459. scanForBots();
  460. const observer = new MutationObserver(mutations => {
  461. mutations.forEach(mutation => {
  462. mutation.addedNodes.forEach(node => {
  463. if (node.nodeType === Node.ELEMENT_NODE) { scanForBots(node); }
  464. });
  465. });
  466. });
  467. observer.observe(document.body, { childList: true, subtree: true });
  468. setInterval(() => { scanForBots(document); }, 3000);
  469. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址