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.

目前為 2025-03-08 提交的版本,檢視 最新版本

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

QingJ © 2025

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