bagscript

bag script with anti bot features + more

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        bagscript
// @description bag script with anti bot features + more
// @version     0.6.1.4
// @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 THREAD_NAME_FILTER = "/bag/";
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();

    if (!threadTitle.innerText.includes(THREAD_NAME_FILTER)) {
      return;
    }

    loadSettings();
    loadToolbar();

    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});
  }
})).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);
  const isNice = isNiceId(id) || isNicePost(post);
  if (isNice) {
    unblurPost(post);
  } else {
    blurPost(post);
  }
}

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 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 = bypassButtonForId(postId(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() {
  // Toolbar container
  const toolbar = document.createElement("div");
  document.querySelector("body").appendChild(toolbar);
  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 = "none";
  toolbarContents.style.flexDirection = "column";
  toolbarContents.style.gap = "1px";
  toolbarContents.style.padding = "1px 1px 0 1px";

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

  // Toolbar toggle button
  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 ? ">>" : "<<";
  }
}

// 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 ${THREAD_NAME_FILTER} 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(THREAD_NAME_FILTER)) {
        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);
}

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

function getSetting(name) {
  return unsafeWindow.localStorage.getItem(name);
}

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 === null) ? {} : 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 bypassButtonForId(id) {
  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();

    unsafeWindow.posting.idsRelation[id].forEach(otherPostInner => {
      unblurPost(otherPostInner.parentElement);
    });
  };

  return bypassButton;
}

// Debug/Test helpers
function mockEmptyCatalogResponse() {
  return Promise.resolve({
    ok: true,
    json: () => Promise.resolve([])
  });
}
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元