bagscript

bag script with anti bot features + more

当前为 2025-05-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        bagscript
// @description bag script with anti bot features + more
// @version     0.7.1
// @license     MIT
// @namespace   9e7f6239-592e-409b-913f-06e11cc5e545
// @include     https://8chan.moe/v/res/*
// @include     https://8chan.se/v/res/*
// @include     https://8chan.moe/barchive/res/*
// @include     https://8chan.se/barchive/res/*
// @include     https://8chan.moe/test/res/*
// @include     https://8chan.se/test/res/*
// @grant       unsafeWindow
// @run-at      document-idle
// ==/UserScript==

// Script settings
const RUDE_FORMATS = ["JPEG", "JPG", "PNG"];
const SPOILER_BORDER = "3px solid red";
const THREAD_LOCKED_AT = 1500;
const URL_PREFIX = window.location.href.split("/").slice(0, 4).join("/");

// Debug settings
const DEBUG_TOOLS_VISIBLE = false;
const DISABLE_YOU_BYPASS = false;
const FORCE_NEXT_THREAD_FAIL = false;

// State
let manualBypass;
let defaultSpoilerSrc;
const settings = {};
let threadsClosed = false;
let toolbarVisible = false;

// Loader
(new MutationObserver((_, observer) => {
  const threadTitle = document.querySelector("div.opHead > span.labelSubject");
  if (threadTitle) {
    observer.disconnect();

    loadSettings();

    const threadTitle = document.querySelector("div.opHead > span.labelSubject").innerText.toUpperCase();
    const titleSetting = settings?.threadSubject?.toUpperCase() ?? "";
    if (threadTitle.includes(titleSetting)) {
      loadToolbar(true);
      subjectMatched();
    } else {
      loadToolbar(false);
    }
  }
})).observe(document, {childList: true, subtree: true});

const subjectMatched = function() {
    const initialPosts = document.querySelectorAll(".postCell");
    if (initialPosts.length >= THREAD_LOCKED_AT) {
      addNextThreadFakePost(0, true);
    }

    initialPosts.forEach((post) => {
      handleSpoilers(post);
    });

    processAllPosts();
    postObserver.observe(document, {childList: true, subtree: true});
}

// New post observer
const postObserver = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType === 1) {
        const isPost = node.classList.contains("postCell");
        const isHoverPost = node.classList.contains("quoteTooltip");
        const isInlineQuote = node.classList.contains("inlineQuote");

        if (isPost) {
          if (settings.findNextThread && !threadsClosed) {
            const totalPostCount = document.querySelector("#postCount").innerText;
            if (totalPostCount >= THREAD_LOCKED_AT) {
              threadsClosed = true;
              addNextThreadFakePost();
            }
          }

          handleSpoilers(node);

          const id = postId(node);
          unsafeWindow.posting.idsRelation[id].forEach((innerPost) => {
            processAllPostsById(id);
          });

          node.querySelectorAll(".quoteLink").forEach((quoteLink) => {
            const quotedId = quoteLink.innerText.substring(2);
            const quotedPost = document.getElementById(quotedId);
            if (quotedPost) {
              processSinglePost(quotedPost);
            }
          });
        } else if (isHoverPost || isInlineQuote) {
          handleSpoilers(node);
          processSinglePost(node);
        }
      }
    }
  }
});

const processSinglePost = function(post) {
  const id = postId(post);
  if (!id) return;

  const isNice = isNiceId(id) || isNicePost(post);
  handlePost(post, isNice);
}

const processAllPosts = function() {
  for (const id in unsafeWindow.posting.idsRelation) {
    processAllPostsById(id);
  }

  document.querySelectorAll(".inlineQuote").forEach((inlineQuote) => {
    processSinglePost(inlineQuote);
  });

  const hoverPost = document.querySelector(".quoteTooltip");
  if (hoverPost) {
    processSinglePost(hoverPost);
  }
}

const processAllPostsById = function(id) {
  const innerPostsById = unsafeWindow.posting.idsRelation[id];
  let isNice = isNiceId(id);

  for (const innerPost of innerPostsById) {
    const post = innerPost.parentElement;

    if (!isNice) {
      isNice = isNicePost(post);
      if (isNice) break;
    }
  }

  innerPostsById.forEach(innerPost => handlePost(innerPost.parentElement, isNice));
}

const isNiceId = function(id) {
  if (!settings.enabled) return true;

  if (manualBypass[id]) return true;

  const innerPostsById = unsafeWindow.posting.idsRelation[id];

  const isOp = innerPostsById.some(innerPost => innerPost.parentElement.classList.contains("opCell"));
  if (isOp) return true;

  const idAboveThreshold = innerPostsById.length >= settings.postThreshold;
  if (idAboveThreshold) return true;

  return false;
}

const isNicePost = function(post) {
  const postIsByYou = DISABLE_YOU_BYPASS ? false : post.querySelector(".youName");
  if (postIsByYou) return true;

  const aboveBlThreshold = post.querySelectorAll(".postInfo > .panelBacklinks > a")?.length >= settings.backlinkThreshold;
  if (aboveBlThreshold) return true;

  if (settings.experimental) {
    const images = post.querySelectorAll("img");

    const noImages = images.length === 0;
    if (noImages) return true;

    const multipleImages = images.length > 1;
    if (multipleImages) return true;

    const hasFunImage = Array.from(images).some((image) => {
      const spoilerImage = image.getAttribute("data-spoiler") === "true"
      if (spoilerImage) return true;

      const format = image?.parentElement?.href?.split("/")?.[4]?.split(".")?.[1]?.toUpperCase();
      if (format) {
        const notRudeImage = !RUDE_FORMATS.includes(format);
        if (notRudeImage) return true;
      }

      return false;
    });

    if (hasFunImage) return true;

    const hasFunText = post.querySelector(".doomText, .moeText, .redText, .pinkText, .diceRoll");
    if (hasFunText) return true;
  }

  return false;
}

const isRudeId = function(id) {
  return settings.experimental && unsafeWindow.posting.idsRelation[id].length === 3;
}

const handlePost = function(post, isNice) {
  let bypassButton = post.querySelector(".bypassButton");

  if (isNice) {
    unblurPost(post);

    if (bypassButton) {
      bypassButton.style.display = "none";
    }
  } else {
    blurPost(post);

    if (bypassButton) {
      bypassButton.style.display = "inline";

      if (isRudeId(postId(post))) {
        bypassButton.style.border = "1px solid red";
      }
    } else {
      bypassButton = bypassButtonForPost(post);
      post.querySelector(".postInfo.title").appendChild(bypassButton);
    }
  }
}

const handleSpoilers = function(post) {
  const spoilers = post.querySelectorAll("img[src*='spoiler'], img[data-spoiler]");

  if (!defaultSpoilerSrc) {
    defaultSpoilerSrc = spoilers[0]?.src;
  }

  spoilers.forEach(spoiler => {
    spoiler.setAttribute("data-spoiler", true);

    if (settings.revealSpoilers) {
      const fileName = spoiler.parentElement.href.split("/")[4].split(".")[0];
      spoiler.src = `/.media/t_${fileName}`;
      spoiler.style.border = SPOILER_BORDER;
    } else {
      spoiler.src = defaultSpoilerSrc;
      spoiler.style.border = "0";
    }
  });
}

const blurPost = function(post) {
  post.style.display = settings.hideFiltered ? "none" : "block";

  post.querySelectorAll("img").forEach((img) => {
    img.style.filter = `blur(${settings.blurStrength}px)`;
  });
}

const unblurPost = function(post) {
  post.style.display = "block";

  post.querySelectorAll("img").forEach((img) => {
    img.style.filter = "";
  });
}

const loadToolbar = function(fullToolbar) {
  document.querySelector(".bagToolbar")?.remove();

  // Toolbar container
  const toolbar = document.createElement("div");
  document.querySelector("body").appendChild(toolbar);
  toolbar.className = "bagToolbar";
  toolbar.style.backgroundColor = "var(--navbar-text-color)";
  toolbar.style.bottom = "0px";
  toolbar.style.color = "var(--navbar-text-color)";
  toolbar.style.display = "flex";
  toolbar.style.gap = "1px";
  toolbar.style.right = "0px";
  toolbar.style.padding = "1px";
  toolbar.style.position = "fixed";

  // Toolbar contents container
  const toolbarContents = document.createElement("div");
  toolbar.appendChild(toolbarContents);
  toolbarContents.style.display = toolbarVisible ? "flex" : "none";
  toolbarContents.style.flexDirection = "column";
  toolbarContents.style.gap = "1px";
  toolbarContents.style.padding = "1px 1px 0 1px";

  // Thread subject input
  const subjectContainer = container();
  toolbarContents.appendChild(subjectContainer);

  const subjectLabel = label("Thread Subject");
  subjectContainer.append(subjectLabel);

  const subjectInput = input(settings.threadSubject);
  subjectInput.size = 10;
  subjectContainer.appendChild(subjectInput);
  subjectInput.onchange = () => {
    settings.threadSubject = subjectInput.value;
    setSetting("bag_threadSubject", settings.threadSubject);

    const threadTitle = document.querySelector("div.opHead > span.labelSubject").innerText.toUpperCase();
    const titleSetting = settings?.threadSubject?.toUpperCase() ?? "";
    if (threadTitle.includes(titleSetting)) {
      loadToolbar(true);
      subjectMatched();
    } else {
      loadToolbar(false);
      postObserver.disconnect();
    }
  }

  if (!fullToolbar) {
    addToggleButton(toolbar, toolbarContents);
    return;
  };

  // Enable checkbox
  const enableContainer = container();
  toolbarContents.appendChild(enableContainer);

  const enableLabel = label("Enable Filter");
  enableContainer.appendChild(enableLabel);

  const enableCheckbox = checkbox(settings.enabled);
  enableContainer.appendChild(enableCheckbox);
  enableCheckbox.onchange = () => {
    settings.enabled = enableCheckbox.checked;
    unsafeWindow.localStorage.setItem("bag_enabled", settings.enabled);

    if (settings.enabled) {
      processAllPosts();
      postObserver.observe(document, {childList: true, subtree: true});
    } else {
      postObserver.disconnect();
      processAllPosts();
    }
  };

  // Post threshold input
  const thresholdContainer = container();
  toolbarContents.appendChild(thresholdContainer);

  const thresholdLabel = label("Post Threshold");
  thresholdContainer.appendChild(thresholdLabel);

  const thresholdInput = input(settings.postThreshold);
  thresholdContainer.appendChild(thresholdInput);
  thresholdInput.onchange = () => {
    settings.postThreshold = thresholdInput.value;
    unsafeWindow.localStorage.setItem("bag_postThreshold", settings.postThreshold);

    processAllPosts();
  };

  // Backlink threshold input
  const blThresholdContainer = container();
  toolbarContents.appendChild(blThresholdContainer);

  const blThresholdLabel = label("Backlink Threshold");
  blThresholdContainer.appendChild(blThresholdLabel);

  const blThresholdInput = input(settings.backlinkThreshold);
  blThresholdContainer.appendChild(blThresholdInput);
  blThresholdInput.onchange = () => {
    settings.backlinkThreshold = blThresholdInput.value;
    setSetting("bag_backlinkThreshold", settings.backlinkThreshold);

    processAllPosts();
  };

  // Blur input
  const blurContainer = container();
  toolbarContents.appendChild(blurContainer);

  const blurLabel = label("Blur Strength");
  blurContainer.appendChild(blurLabel);

  const blurInput = input(settings.blurStrength);
  blurContainer.appendChild(blurInput);
  blurInput.onchange = () => {
    settings.blurStrength = blurInput.value;
    unsafeWindow.localStorage.setItem("bag_blurStrength", settings.blurStrength);

    processAllPosts();
  };

  // Experimental checkbox
  const experimentalContaner = container();
  toolbarContents.appendChild(experimentalContaner);

  const experimentalLabel = label("Experimental Heuristics");
  experimentalContaner.appendChild(experimentalLabel);

  const experimentalCheckbox = checkbox(settings.experimental);
  experimentalContaner.appendChild(experimentalCheckbox);
  experimentalCheckbox.onchange = () => {
    settings.experimental = experimentalCheckbox.checked;
    unsafeWindow.localStorage.setItem("bag_experimental", settings.experimental);

    if (!settings.experimental) {
      document.querySelectorAll('.innerPost').forEach(innerPost => {
        innerPost.style.borderRight = "1px solid var(--horizon-sep-color)";
      });

      document.querySelectorAll(".bypassButton").forEach(bypassButton => {
        bypassButton.style.border = "1px solid var(--horizon-sep-color)";
      });
    }

    processAllPosts();
  };

  // Hide filtered checkbox
  const hideContainer = container();
  toolbarContents.appendChild(hideContainer);

  const hideLabel = label("Hide Filtered");
  hideContainer.appendChild(hideLabel);

  const hideCheckbox = checkbox(settings.hideFiltered);
  hideContainer.appendChild(hideCheckbox);
  hideCheckbox.onchange = () => {
    settings.hideFiltered = hideCheckbox.checked;
    unsafeWindow.localStorage.setItem("bag_hideFiltered", settings.hideFiltered);

    processAllPosts();
  };

  // Reveal spoilers checkbox
  const revealContainer = container();
  toolbarContents.appendChild(revealContainer);

  const revealLabel = label("Reveal Spoilers");
  revealContainer.appendChild(revealLabel);

  const revealCheckbox = checkbox(settings.revealSpoilers);
  revealContainer.appendChild(revealCheckbox);
  revealCheckbox.onchange = () => {
    settings.revealSpoilers = revealCheckbox.checked;
    setSetting("bag_revealSpoilers", settings.revealSpoilers);

    document.querySelectorAll(".postCell").forEach(post => handleSpoilers(post));
  };

  // Next thread checkbox
  const nextThreadContainer = container();
  toolbarContents.appendChild(nextThreadContainer);

  const nextThreadLabel = label("Find Next Thread");
  nextThreadContainer.appendChild(nextThreadLabel);

  const nextThreadCheckbox = checkbox(settings.findNextThread);
  nextThreadContainer.appendChild(nextThreadCheckbox);
  nextThreadCheckbox.onchange = () => {
    settings.findNextThread = nextThreadCheckbox.checked;
    setSetting("bag_findNextThread", settings.findNextThread);
  };

  // Debug tools
  if (DEBUG_TOOLS_VISIBLE) {
    const fakePostButton = button();
    toolbarContents.appendChild(fakePostButton);
    fakePostButton.innerText = "Test Fake Post";
    fakePostButton.style.backgroundColor = "var(--background-color)";
    fakePostButton.onclick = () => {
      const url = `${URL_PREFIX}/res/1289960.html`
      addFakePost(`fake post test\r\n<a href="${url}">${url}</a>`);
    }

    const triggerThreadCheckButton = button();
    toolbarContents.appendChild(triggerThreadCheckButton);
    triggerThreadCheckButton.innerText = "Test Thread Finder";
    triggerThreadCheckButton.style.backgroundColor = "var(--background-color)";
    triggerThreadCheckButton.onclick = () => {
      addNextThreadFakePost(0, true);
    }
  }

  addToggleButton(toolbar, toolbarContents);
}

// Post helpers
const postId = function(post) {
  return post?.querySelector('.labelId')?.innerText;
}

const addFakePost = function(contents) {
  const outer = document.createElement("div");
  document.querySelector(".divPosts").appendChild(outer);
  outer.className = "fakePost";
  outer.style.marginBottom = "0.25em";

  const inner = document.createElement("div");
  outer.appendChild(inner);
  inner.className = "innerPost";

  const message = document.createElement("div");
  inner.appendChild(message);
  message.className = "divMessage";
  message.innerHTML = contents;

  return inner;
}

const addNextThreadFakePost = function(initialQueryDelay, includeAutoSage) {
  document.querySelector(".nextThread")?.remove();

  const fakePost = addFakePost(`Searching for next ${settings.threadSubject} thread...`);
  fakePost.classList.add("nextThread");

  const fakePostMessage = document.querySelector(".nextThread .divMessage");
  const delay = FORCE_NEXT_THREAD_FAIL ? 500 : 30000;

  setTimeout(async () => {
    const found = FORCE_NEXT_THREAD_FAIL
      ? false
      : await queryNextThread(fakePost, fakePostMessage, includeAutoSage);

    if (!found) {
      fakePostMessage.innerHTML += `\r\nThread not found, retrying in 30s`;

      let retryCount = 8;
      const interval = setInterval(async () => {
        if (retryCount-- < 0) {
          clearInterval(interval);
          fakePostMessage.innerHTML += "\r\nNEXT THREAD NOT FOUND"
          fakePost.style.border = "5px solid red";
          return;
        }

        const retryFound = await queryNextThread(fakePost, fakePostMessage, includeAutoSage);
        if (retryFound) {
          clearInterval(interval);
        } else {
          fakePostMessage.innerHTML += `\r\nThread not found, retrying in 30s`;
        }
      }, delay);
    }
  }, initialQueryDelay ?? 60000);
}

// returns true if no more retries should be attempted
const queryNextThread = async function(fakePost, fakePostMessage, includeAutoSage) {
  // Try to fix issues people were having where fakePostMessage was undefined even with the fake post present.
  // Not sure what the actual cause is, haven't been able to replicate
  if (!fakePost) fakePost = document.querySelector(".nextThread");
  if (!fakePostMessage) fakePostMessage = document.querySelector(".nextThread .divMessage");

  const catalogUrl = barchiveToV(`${URL_PREFIX}/catalog.json`);
  unsafeWindow.console.log("searching for next thread", catalogUrl);

  const catalog = FORCE_NEXT_THREAD_FAIL
    ? await mockEmptyCatalogResponse()
    : await fetch(catalogUrl);

  if (catalog.ok) {
    const threads = await catalog.json();
    for (const thread of threads) {
      const notAutoSage = includeAutoSage || !thread.autoSage;
      if (notAutoSage && thread.subject?.includes(settings.threadSubject)) {
        const url = barchiveToV(`${URL_PREFIX}/res/${thread.threadId}.html`);
        fakePostMessage.innerHTML = `${thread.subject} [${thread.postCount ?? 1} posts]:\r\n<a href=${url}>${url}</a>`;
        fakePost.style.border = "5px solid green";
        return true;
      }
    }

    return false;
  } else {
    fakePostMessage.innerHTML = "ERROR WHILE LOOKING FOR NEXT THREAD";
    fakePost.style.border = "5px solid red";
    return true;
  }
}

const barchiveToV = function(url) {
  return url.replace("barchive", "v");
}

// LocalStorage Helpers
const loadSettings = function() {
  manualBypass = getManualBypass();

  settings.backlinkThreshold = getIntSetting("bag_backlinkThreshold", 3);
  settings.blurStrength = getIntSetting("bag_blurStrength", 10);
  settings.findNextThread = getBoolSetting("bag_findNextThread", true);
  settings.enabled = getBoolSetting("bag_enabled", true);
  settings.experimental = getBoolSetting("bag_experimental", true);
  settings.hideFiltered = getBoolSetting("bag_hideFiltered", false);
  settings.postThreshold = getIntSetting("bag_postThreshold", 4);
  settings.revealSpoilers = getBoolSetting("bag_revealSpoilers", false);
  settings.threadSubject = getSetting("bag_threadSubject", "/bag/");
}

function setSetting(name, value) {
  unsafeWindow.localStorage.setItem(name, value);
}

function getSetting(name, defaultValue) {
  const value = unsafeWindow.localStorage.getItem(name);
  if (value === null) return defaultValue;
  return value;
}

function getBoolSetting(name, defaultValue) {
  const value = getSetting(name);
  if (value === null) return defaultValue;
  return value == "true";
}

function getIntSetting(name, defaultValue) {
  const value = getSetting(name);
  if (value === null) return defaultValue;
  return parseInt(value);
}

function getManualBypass() {
  const bypassVar = `bag_bypass_${unsafeWindow.api.threadId}`;
  const bp = getSetting(bypassVar);
  return (!bp) ? {} : JSON.parse(bp);
}

function setManualBypass() {
  const bypassVar = `bag_bypass_${unsafeWindow.api.threadId}`;
  const bypassData = JSON.stringify(manualBypass);
  unsafeWindow.localStorage.setItem(bypassVar, bypassData);
}

// HTML Helpers
function container() {
  const container = document.createElement("div");
  container.style.alignItems = "center";
  container.style.backgroundColor = "var(--background-color)";
  container.style.display = "flex";
  container.style.gap = "0.25rem";
  container.style.justifyContent = "space-between";
  container.style.padding = "0.25rem";
  return container;
}

function label(text) {
  const label = document.createElement("div");
  label.innerText = text;
  label.style.color = "white";
  return label;
}

function checkbox(initialValue) {
  const checkbox = document.createElement("input");
  checkbox.type = "checkbox";
  checkbox.style.cursor = "pointer";
  checkbox.checked = initialValue;
  return checkbox;
}

function input(initialValue) {
  const input = document.createElement("input");
  input.size = 4;
  input.value = initialValue;
  return input;
}

function button() {
  const button = document.createElement("div");
  button.style.alignItems = "center";
  button.style.color = "var(--link-color)";
  button.style.cursor = "pointer";
  button.style.display = "flex";
  button.style.padding = "0.25rem 0.75rem";
  button.style.userSelect = "none";
  return button;
}

function bypassButtonForPost(post) {
  const id = postId(post);
  if (!id) return;

  const border = isRudeId(id)
    ? "1px solid red"
    : "1px solid var(--horizon-sep-color)";

  const bypassButton = button();
  bypassButton.className = "bypassButton";
  bypassButton.innerText = "+";
  bypassButton.style.display = "inline";
  bypassButton.style.marginLeft = "auto";
  bypassButton.style.border = border;
  bypassButton.onclick = () => {
    bypassButton.style.display = "none";
    manualBypass[id] = true;
    setManualBypass();

    processSinglePost(post);
    processAllPostsById(id);
  };

  return bypassButton;
}

function addToggleButton(toolbar, toolbarContents) {
  const toggleButton = button();
  toolbar.appendChild(toggleButton);
  toggleButton.innerText = "<<"
  toggleButton.style.backgroundColor = "var(--background-color)"
  toggleButton.onclick = () => {
    toolbarVisible = !toolbarVisible;
    toolbarContents.style.display = toolbarVisible ? "flex" : "none";
    toggleButton.innerText = toolbarVisible ? ">>" : "<<";
  }
}

// Debug/Test helpers
function mockEmptyCatalogResponse() {
  return Promise.resolve({
    ok: true,
    json: () => Promise.resolve([])
  });
}