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