// ==UserScript== // @name bagscript // @description bag script with anti bot features + more // @version 0.9.0.3 // @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 // @downloadURL none // ==/UserScript== // Script settings const FUN_TEXT_SELECTOR = ".doomText, .moeText, .redText, .pinkText, .diceRoll, .echoText"; const RUDE_FORMATS = ["JPEG", "JPG", "PNG"]; const THREAD_LOCKED_AT = 1500; const URL_PREFIX = window.location.href.split("/").slice(0, 4).join("/"); // Colors const BAN_BUTTON_BORDER = "1px solid red"; const SPOILER_BORDER = "3px solid red"; const THREAD_FOUND_BORDER = "5px solid green"; const THREAD_NOT_FOUND_BORDER = "5px solid red"; // Janny tool settings const BOT_BAN_DURATION = "3d"; const BOT_BAN_REASON = "bot"; const PIZZA_BAN_DURATION = ""; const PIZZA_BAN_REASON = "pizza"; // Debug settings const DEBUG_TOOLS_VISIBLE = false; const FORCE_NEXT_THREAD_FAIL = false; // Tooltips / Info / Etc const NOT_A_JANNY = "You aren't a janny dumbass." const BOT_BAN_BUTTON_WARNING = "WARNING: The ban buttons will immediately issue a ban + " + "delete by IP for the poster WITH NO CONFIRMATION. The ban reason and duration can be " + "set in the script (refresh after modifying). Are you sure you want to turn this on?"; // State let checkedJannyStatus = false; let manualBypass; let defaultSpoilerSrc; let loggedInAsJanny = false; const settings = {}; let threadsClosed = false; let menuVisible = false; // Loading loadSettings(); loadMenu(); const loaderObserver = new MutationObserver((_, observer) => { const loaded = document.querySelector("div.opHead"); if (loaded) { observer.disconnect(); const initialPosts = document.querySelectorAll(".postCell"); if (initialPosts.length >= THREAD_LOCKED_AT) { threadsClosed = true; addNextThreadFakePost(0, true); } processAllPosts(); checkIfJanny((isJanny) => { if (isJanny) { document.querySelectorAll(".jannyTab, .jannyTools").forEach((e) => e.style.display = "flex"); } }); postObserver.observe(document, {childList: true, subtree: true}); } }); if (settings.enabled) { loaderObserver.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"); if (isPost) { if (settings.findNextThread && !threadsClosed) { const totalPostCount = document.querySelector("#postCount").innerText; if (totalPostCount >= THREAD_LOCKED_AT) { threadsClosed = true; addNextThreadFakePost(); } } 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 { const isHoverPost = node.classList.contains("quoteTooltip"); const isInlineQuote = node.classList.contains("inlineQuote"); if (isHoverPost || isInlineQuote) { processSinglePost(node); } } } } } }); // Post handling function processAllPosts() { 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); } } function processAllPostsById(id) { const innerPostsById = unsafeWindow.posting.idsRelation[id]; let isNice = false; for (const innerPost of innerPostsById) { if (isNicePost(innerPost.parentElement)) { isNice = true; break; } } unsafeWindow.posting.idsRelation[id].forEach((innerPost) => { processSinglePost(innerPost.parentElement, isNice); }); } function processSinglePost(post, isNiceOverride) { // Handle spoilers const images = post.querySelectorAll(".uploadCell img"); images.forEach((image) => { const isSpoiler = image.src.includes("spoiler") || image.getAttribute("data-spoiler"); if (isSpoiler) { defaultSpoilerSrc ??= image.src; if (settings.enabled && settings.revealSpoilers) { image.setAttribute("data-spoiler", true); const sha256 = image.parentElement.href.split("/")[4].split(".")[0]; image.src = `/.media/t_${sha256}`; image.style.border = SPOILER_BORDER; } else { image.src = defaultSpoilerSrc; image.style.border = "0"; } } }); if (post.classList.contains("opCell")) return; // Handle rude posts let bypassButton = post.querySelector(".bypassButton"); let jannyTools = post.querySelector(".jannyTools"); const isNice = isNiceOverride ? true : isNicePost(post); if (!settings.enabled || isNice) { // Unblur post.style.display = "block"; post.querySelectorAll("img").forEach((img) => { img.style.filter = ""; }); if (bypassButton) { bypassButton.style.display = "none"; } if (jannyTools) { //jannyTools.style.display = "none"; jannyTools.remove(); } } else { // Blur post.style.display = settings.hideFiltered ? "none" : "block"; images.forEach((img) => { img.style.filter = `blur(${settings.blurStrength}px)`; }); if (bypassButton) { bypassButton.style.display = "inline"; } else { bypassButton = bypassButtonForPost(post); post.querySelector(".postInfo.title").appendChild(bypassButton); } addJannyToolsToPost(post); } } function isNicePost(post) { if (post.classList.contains("opCell")) { return false; } const id = postId(post); if (!id) return false; if (manualBypass[id]) return true; const innerPosts = unsafeWindow.posting.idsRelation[id]; const idAboveThreshold = innerPosts.length >= settings.postThreshold; if (idAboveThreshold) return true; if (settings.whitelist.isYou) { const postIsByYou = post.querySelector(".youName"); if (postIsByYou) return true; } if (settings.whitelist.isOp) { const isOp = document.querySelector(".opCell .labelId").innerText === id; if (isOp) return true; } const backlinks = post.querySelector(".postInfo").querySelectorAll(".panelBacklinks > a"); const aboveBlThreshold = backlinks.length >= settings.backlinkThreshold; if (aboveBlThreshold) return true; if (settings.whitelist.hasFunText) { const hasFunText = post.querySelector(FUN_TEXT_SELECTOR); if (hasFunText) return true; } // Image heuristics const images = post.querySelectorAll(".uploadCell img:not(.imgExpanded)"); if (settings.whitelist.hasNoImages) { const noImages = images.length === 0; if (noImages) return true; } /* if (settings.whitelist.hasMultipleImages) { const multipleImages = images.length > 1; if (multipleImages) return true; } */ let spoilerCount = 0; for (const image of images) { if (settings.whitelist.hasSpoilerImage) { const spoilerImage = image.getAttribute("data-spoiler") === "true" if (spoilerImage) spoilerCount++; } if (settings.whitelist.hasGoodExtension) { const format = image?.parentElement?.href?.split("/")?.[4]?.split(".")?.[1]?.toUpperCase(); if (format) { const notRudeImage = !RUDE_FORMATS.includes(format); if (notRudeImage) return true; } } } if (images.length > 0 && spoilerCount === images.length) return true; return false; } // Menu function loadMenu() { document.querySelector(".bagMenu")?.remove(); // Menu container const menu = document.createElement("div"); document.querySelector("body").appendChild(menu); menu.className = "bagMenu"; menu.style.bottom = "0px"; menu.style.color = "var(--navbar-text-color)"; menu.style.display = "flex"; menu.style.gap = "1px"; menu.style.right = "0px"; menu.style.padding = "1px"; menu.style.position = "fixed"; // Menu contents container const menuContents = document.createElement("div"); menu.appendChild(menuContents); menuContents.style.backgroundColor = "var(--navbar-text-color)"; menuContents.style.border = "1px solid var(--navbar-text-color)"; menuContents.style.display = menuVisible ? "flex" : "none"; menuContents.style.flexDirection = "column"; menuContents.style.gap = "1px"; // Tabs container const tabs = document.createElement("div"); tabs.style.display = "flex"; tabs.style.gap = "1px"; buildGeneralTab(tabs, menuContents); buildFilterTab(tabs, menuContents); buildFinderTab(tabs, menuContents); buildJannyTab(tabs, menuContents); buildDebugTab(tabs, menuContents); menuContents.appendChild(tabs); addToggleButton(menu, menuContents); } function buildGeneralTab(tabsContainer, contentContainer) { const generalTab = makeTab("General"); tabsContainer.appendChild(generalTab); const generalTabContainer = makeTabContainer("General"); contentContainer.appendChild(generalTabContainer); // Enable checkbox const enableContainer = makeContainer(); generalTabContainer.appendChild(enableContainer); const enableLabel = makeLabel("Enable Script"); enableContainer.appendChild(enableLabel); const enableCheckbox = makeCheckbox(settings.enabled); enableContainer.appendChild(enableCheckbox); enableCheckbox.onchange = () => { settings.enabled = enableCheckbox.checked; unsafeWindow.localStorage.setItem("bag_enabled", settings.enabled); if (settings.enabled) { loaderObserver.observe(document, {childList: true, subtree: true}); } else { postObserver.disconnect(); processAllPosts(); } }; // Reveal spoilers checkbox const revealContainer = makeContainer(); generalTabContainer.appendChild(revealContainer); const revealLabel = makeLabel("Reveal Spoilers"); revealContainer.appendChild(revealLabel); const revealCheckbox = makeCheckbox(settings.revealSpoilers); revealContainer.appendChild(revealCheckbox); revealCheckbox.onchange = () => { settings.revealSpoilers = revealCheckbox.checked; setSetting("bag_revealSpoilers", settings.revealSpoilers); processAllPosts(); }; } function buildFilterTab(tabsContainer, contentContainer) { const filterTab = makeTab("Filter"); tabsContainer.appendChild(filterTab); const filterTabContainer = makeTabContainer("Filter"); contentContainer.appendChild(filterTabContainer); // Blur input const blurContainer = makeContainer(); filterTabContainer.appendChild(blurContainer); const blurLabel = makeLabel("Blur Strength"); blurContainer.appendChild(blurLabel); const blurInput = makeInput(settings.blurStrength); blurContainer.appendChild(blurInput); blurInput.onchange = () => { settings.blurStrength = blurInput.value; unsafeWindow.localStorage.setItem("bag_blurStrength", settings.blurStrength); processAllPosts(); }; // Hide filtered checkbox const hideContainer = makeContainer(); filterTabContainer.appendChild(hideContainer); const hideLabel = makeLabel("Hide Filtered"); hideContainer.appendChild(hideLabel); const hideCheckbox = makeCheckbox(settings.hideFiltered); hideContainer.appendChild(hideCheckbox); hideCheckbox.onchange = () => { settings.hideFiltered = hideCheckbox.checked; unsafeWindow.localStorage.setItem("bag_hideFiltered", settings.hideFiltered); processAllPosts(); }; // Whitelist label const whitelistContainer = makeContainer(); filterTabContainer.appendChild(whitelistContainer); const whitelistLabel = makeLabel("------- Auto Whitelist -------"); whitelistContainer.appendChild(whitelistLabel); whitelistLabel.style.textAlign = "center"; whitelistLabel.style.width = "100%"; // Post threshold input const thresholdContainer = makeContainer(); filterTabContainer.appendChild(thresholdContainer); const thresholdLabel = makeLabel("ID Post Count"); thresholdContainer.appendChild(thresholdLabel); const thresholdInput = makeInput(settings.postThreshold); thresholdContainer.appendChild(thresholdInput); thresholdInput.onchange = () => { settings.postThreshold = thresholdInput.value; unsafeWindow.localStorage.setItem("bag_postThreshold", settings.postThreshold); processAllPosts(); }; // Backlink threshold input const blThresholdContainer = makeContainer(); filterTabContainer.appendChild(blThresholdContainer); const blThresholdLabel = makeLabel("Post Quoted Count"); blThresholdContainer.appendChild(blThresholdLabel); const blThresholdInput = makeInput(settings.backlinkThreshold); blThresholdContainer.appendChild(blThresholdInput); blThresholdInput.onchange = () => { settings.backlinkThreshold = blThresholdInput.value; setSetting("bag_backlinkThreshold", settings.backlinkThreshold); processAllPosts(); }; filterTabContainer.appendChild(makeHeuristicCheckbox("Is (You)", "isYou")); filterTabContainer.appendChild(makeHeuristicCheckbox("Is OP", "isOp")); filterTabContainer.appendChild(makeHeuristicCheckbox("Has Fun Text", "hasFunText")); filterTabContainer.appendChild(makeHeuristicCheckbox("Has No Images", "hasNoImages")); //filterTabContainer.appendChild(makeHeuristicCheckbox("Has 2+ Images", "hasMultipleImages")); filterTabContainer.appendChild(makeHeuristicCheckbox("Has Only Spoiler Images", "hasSpoilerImage")); filterTabContainer.appendChild(makeHeuristicCheckbox("Has Good File Ext", "hasGoodExtension")); } function buildFinderTab(tabsContainer, contentContainer) { const finderTab = makeTab("Finder"); tabsContainer.appendChild(finderTab); const finderTabContainer = makeTabContainer("Finder"); contentContainer.appendChild(finderTabContainer); // Thread finder checkbox const nextThreadContainer = makeContainer(); finderTabContainer.appendChild(nextThreadContainer); const nextThreadLabel = makeLabel("Enable Thread Finder"); nextThreadContainer.appendChild(nextThreadLabel); const nextThreadCheckbox = makeCheckbox(settings.findNextThread); nextThreadContainer.appendChild(nextThreadCheckbox); nextThreadCheckbox.onchange = () => { settings.findNextThread = nextThreadCheckbox.checked; setSetting("bag_findNextThread", settings.findNextThread); }; // Thread subject input const subjectContainer = makeContainer(); finderTabContainer.appendChild(subjectContainer); const subjectLabel = makeLabel("Thread Subject"); subjectContainer.append(subjectLabel); const subjectInput = makeInput(settings.threadSubject); subjectInput.className = "subjectInput"; subjectInput.size = 10; subjectContainer.appendChild(subjectInput); subjectInput.onchange = () => { settings.threadSubject = subjectInput.value; setSetting("bag_threadSubject", settings.threadSubject); } } function buildJannyTab(tabsContainer, contentContainer) { const jannyTab = makeTab("Janny"); jannyTab.classList.add("jannyTab"); jannyTab.style.display = "none"; tabsContainer.appendChild(jannyTab); const jannyTabContainer = makeTabContainer("Janny"); contentContainer.appendChild(jannyTabContainer); // Bot ban checkbox const jannyToolsContainer = makeContainer(); jannyTabContainer.appendChild(jannyToolsContainer); const jannyToolsLabel = makeLabel("Janny Tools"); jannyToolsContainer.appendChild(jannyToolsLabel); const jannyToolsCheckbox = makeCheckbox(settings.showJannyTools); jannyToolsContainer.appendChild(jannyToolsCheckbox); jannyToolsCheckbox.onchange = () => { if (jannyToolsCheckbox.checked) { if (!loggedInAsJanny) { alert(NOT_A_JANNY); jannyToolsCheckbox.checked = false; return; } if (!confirm(BOT_BAN_BUTTON_WARNING)) { jannyToolsCheckbox.checked = false; return; } } settings.showJannyTools = jannyToolsCheckbox.checked; setSetting("bag_showJannyTools", settings.showJannyTools); processAllPosts(); } } function buildDebugTab(tabsContainer, contentContainer) { if (!DEBUG_TOOLS_VISIBLE) return; const debugTab = makeTab("Debug"); tabsContainer.appendChild(debugTab); const debugTabContainer = makeTabContainer("Debug"); contentContainer.appendChild(debugTabContainer); const fakePostButton = makeButton(); debugTabContainer.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${url}`); } const triggerThreadCheckButton = makeButton(); debugTabContainer.appendChild(triggerThreadCheckButton); triggerThreadCheckButton.innerText = "Test Thread Finder"; triggerThreadCheckButton.style.backgroundColor = "var(--background-color)"; triggerThreadCheckButton.onclick = () => { addNextThreadFakePost(0, true); } const clearStorageButton = makeButton(); debugTabContainer.appendChild(clearStorageButton); clearStorageButton.innerText = "Clear Storage"; clearStorageButton.style.backgroundColor = "var(--background-color)"; clearStorageButton.onclick = () => { Object.keys(localStorage).filter(x => x.startsWith("bag_")).forEach((x) => localStorage.removeItem(x)); location.reload(); } } // Post helpers function postId(post) { return post?.querySelector('.labelId')?.innerText; } function addFakePost(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; } function addNextThreadFakePost(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 = THREAD_NOT_FOUND_BORDER; 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 async function queryNextThread(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${url}`; fakePost.style.border = THREAD_FOUND_BORDER; return true; } } return false; } else { fakePostMessage.innerHTML = "ERROR WHILE LOOKING FOR NEXT THREAD"; fakePost.style.border = THREAD_NOT_FOUND_BORDER; return true; } } // LocalStorage Helpers function loadSettings() { // State manualBypass = getManualBypass(); settings.activeTab = getStringSetting("bag_activeTab", "General"); // General settings settings.enabled = getBoolSetting("bag_enabled", true); settings.revealSpoilers = getBoolSetting("bag_revealSpoilers", false); // Filter settings settings.postThreshold = getIntSetting("bag_postThreshold", 4); settings.backlinkThreshold = getIntSetting("bag_backlinkThreshold", 3); settings.blurStrength = getIntSetting("bag_blurStrength", 10); settings.hideFiltered = getBoolSetting("bag_hideFiltered", false); // Heuristic settings settings.whitelist = {}; settings.whitelist.isYou = getBoolSetting("bag_whitelist_isYou", true); settings.whitelist.isOp = getBoolSetting("bag_whitelist_isOp", true); settings.whitelist.hasFunText = getBoolSetting("bag_whitelist_hasFunText", true); settings.whitelist.hasNoImages = getBoolSetting("bag_whitelist_hasNoImages", true); settings.whitelist.hasMultipleImages = getBoolSetting("bag_whitelist_hasMultipleImages", false); settings.whitelist.hasSpoilerImage = getBoolSetting("bag_whitelist_hasSpoilerImage", true); settings.whitelist.hasGoodExtension = getBoolSetting("bag_whitelist_hasGoodExtension", true); // Thread finder settings settings.findNextThread = getBoolSetting("bag_findNextThread", true); settings.threadSubject = getStringSetting("bag_threadSubject", "/bag/"); // Janny Settings settings.showJannyTools = getBoolSetting("bag_showJannyTools", 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 getStringSetting(name, defaultValue) { const value = getSetting(name); if (value === null) return defaultValue; return 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 makeContainer() { 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.1rem"; return container; } function makeLabel(text) { const label = document.createElement("div"); label.innerText = text; label.style.color = "var(--text-color)"; label.style.userSelect = "none"; return label; } function makeCheckbox(initialValue) { const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.style.cursor = "pointer"; checkbox.checked = initialValue; return checkbox; } function makeHeuristicCheckbox(label, setting) { const container = makeContainer(); const labelElement = makeLabel(label); container.appendChild(labelElement); const checkbox = makeCheckbox(settings.whitelist[setting]); container.appendChild(checkbox); checkbox.onchange = () => { settings.whitelist[setting] = checkbox.checked; localStorage.setItem(`bag_whitelist_${setting}`, settings.whitelist[setting]); processAllPosts(); }; return container; } function makeInput(initialValue) { const input = document.createElement("input"); input.size = 4; input.value = initialValue; input.style.border = "1px solid var(--navbar-text-color)"; return input; } function makeButton() { 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 bypassButton = makeButton(); bypassButton.innerText = "+"; bypassButton.className = "bypassButton"; bypassButton.style.border = "1px solid var(--horizon-sep-color)"; bypassButton.style.display = "inline"; bypassButton.style.marginLeft = "1rem"; bypassButton.onclick = () => { bypassButton.style.display = "none"; manualBypass[id] = true; setManualBypass(); processSinglePost(post); processAllPostsById(id); }; return bypassButton; } function addJannyToolsToPost(post) { const innerPost = post.querySelector(".innerPost"); const shouldShow = loggedInAsJanny && settings.showJannyTools; let tools = post.querySelector(".jannyTools"); if (tools) { tools.style.display = shouldShow ? "flex" : "none"; } else { tools = document.createElement("div"); tools.className = "jannyTools"; innerPost.appendChild(tools); tools.style.display = shouldShow ? "flex" : "none"; tools.style.paddingTop = "0.25rem"; tools.style.gap = "1rem"; tools.style.justifyContent = "flex-end"; tools.style.width = "100%"; addBanButtonToTools(tools, post, "Bot Ban", "bot", "3d"); addBanButtonToTools(tools, post, "🍕", "pizza", ""); } return tools; } function addBanButtonToTools(container, post, buttonText, banReason, banLength) { const innerPost = post.querySelector(".innerPost"); // Bot ban button let banButton = post.querySelector(".banButton." + banReason); if (!banButton) { banButton = document.createElement("div"); banButton.className = `banButton ${banReason}`; container.appendChild(banButton); banButton.innerText = buttonText; banButton.style.border = BAN_BUTTON_BORDER; banButton.style.cursor = "pointer"; banButton.style.display = "block"; banButton.style.margin = "0"; banButton.style.padding = "0.25rem"; banButton.style.userSelect = "none"; banButton.onclick = () => { const postId = innerPost.querySelector("a.linkQuote").innerText; const dummy = document.createElement("div"); postingMenu.applySingleBan( "", 3, banReason, false, 0, banLength, false, true, "v", api.threadId, postId, innerPost, dummy ); } } return banButton; } function addToggleButton(menu, menuContents) { const toggleButton = makeButton(); menu.appendChild(toggleButton); toggleButton.innerText = "<<" toggleButton.style.alignSelf = "flex-end"; toggleButton.style.backgroundColor = "var(--background-color)"; toggleButton.style.border = "1px solid var(--navbar-text-color)"; toggleButton.onclick = () => { menuVisible = !menuVisible; menuContents.style.display = menuVisible ? "flex" : "none"; toggleButton.innerText = menuVisible ? ">>" : "<<"; } } function makeTab(tabName) { const isActive = settings.activeTab === tabName; const tab = document.createElement("div"); tab.innerText = tabName; tab.className = "bagTab" tab.style.backgroundColor = "var(--background-color)"; tab.style.color = isActive ? "var(--link-color)" : "var(--text-color)"; tab.style.cursor = "pointer"; tab.style.flexGrow = "1"; tab.style.padding = "0.25rem 0.75rem"; tab.style.userSelect = "none"; tab.onclick = () => { settings.activeTab = tabName; setSetting("bag_activeTab", settings.activeTab); // Tab document.querySelectorAll(".bagTab").forEach((tab) => { tab.style.color = "var(--text-color)"; }); tab.style.color = "var(--link-color)"; // Tab container document.querySelectorAll(".bagTabContainer").forEach((tabContainer) => { tabContainer.style.display = "none"; }); document.querySelector(`.bagTabContainer[data-tab="${tabName}"]`).style.display = "flex"; }; return tab; } function makeTabContainer(tabName) { const isActive = settings.activeTab === tabName; const tabContainer = document.createElement("div"); tabContainer.className = "bagTabContainer"; tabContainer.setAttribute("data-tab", tabName) tabContainer.style.display = isActive ? "flex" : "none"; tabContainer.style.flexDirection = "column" tabContainer.style.gap = "1px"; return tabContainer; } // Misc helpers function barchiveToV(url) { return url.replace("barchive", "v"); } function checkIfJanny(callback) { if (checkedJannyStatus) { if (callback) callback(loggedInAsJanny); } else { checkedJannyStatus = true; api.formApiRequest("account", {}, (status, data) => { if (status !== "ok") return; loggedInAsJanny = data.ownedBoards?.includes(api.boardUri) || data.volunteeredBoards?.includes(api.boardUri); if (callback) callback(loggedInAsJanny); }, true); } } // Debug/Test helpers function mockEmptyCatalogResponse() { return Promise.resolve({ ok: true, json: () => Promise.resolve([]) }); }