您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Blocks NSFW bonk maps
当前为
// ==UserScript== // @name Bonk NSFW map filter // @namespace salama.xyz // @author Salama // @version 1.8 // @match https://*.bonk.io/gameframe-release.html // @match https://*.bonkisback.io/gameframe-release.html // @supportURL https://discord.gg/Dj6usq7ww3 // @grant none // @description Blocks NSFW bonk maps // @run-at document-end // ==/UserScript== //////////////////////// //// SETTINGS //// //////////////////////// /* Optionally depends on code injector and BonkLIB * ---------------------------------------------------------------- * https://greasyfork.org/en/scripts/433861-code-injector-bonk-io * - Adds warning to map suggestions * - Blurs or blacks out map in lobby and in game * ---------------------------------------------------------------- * https://greasyfork.org/en/scripts/508104-bonklib * - Adds GUI settings * ---------------------------------------------------------------- */ // This script connects to GitHub to get an up to date list. // Commas in front for ease of use // true = yes // false = no const settings = { HIDE_NSFW_REPLAYS: true ,DISABLE_REPLAYS: false // If false, maps will be blurred instead ,HIDE_MAPS_FROM_MAP_SELECTOR: true // If true, map details won't be blurred ,BLUR_ONLY_MAP_PREVIEW: false ,UNBLUR_MAP_ON_MOUSE_HOVER: false ,INCLUDE_REMIXES_OF_NSFW_MAPS: true /* You can enable this in case injection fails due to a * bonk update. The mod will still work for its main purpose */ ,DISABLE_INJECTION_FAIL_WARNING: false ,WARN_ABOUT_MAP_REQUESTS: true ,HIDE_GAME_ON_NSFW: true // Blocklist is cached for 4 * 60 sec = 4 minutes ,CACHE_DURATION: 4 * 60 ,AUTHOR_BLOCKLIST: [] } const guiSettings = { noWindow: true, // Must be defined before bonkHUD.createMod settingsContent: null, // Version (optional) bonkLIBVersion: "1.1.3", modVersion: "1.8", } //////////////////////// /// CODE //// //////////////////////// 'use strict'; const NSFWLIST_VERSION = 2; const global = { cacheTime: 0, NSFWList: [], NSFWMaps: new Set(), replays: [], ignoreNextReport: false, injected: false }; function addToNSFWList(id, author = "fi", name = "fi") { if (global.injected) { global.NSFWMaps.add([id.toString(), author, name].join("ff")); } else { global.NSFWMaps.add(id.toString()); } } function getFromNSFWList(id, author = "fi", name = "fi") { if (global.injected) { return global.NSFWMaps.has([id.toString(), author, name].join("ff")); } else { return global.NSFWMaps.has(id.toString()); } } function requestHandler(original) { return function(url,body,success,type) { if (global.ignoreNextReport && url.endsWith("/replay_report.php") ) { global.ignoreNextReport = false; return { done: () => { return { fail: () => {} } } } } if (settings.DISABLE_REPLAYS && url.endsWith("/replay_get.php") ) { return { done: () => { return { fail: () => {} } } } } // Send request const response = original.apply(this, arguments); // Hijack response callback const responseDone = response.done; response.done = function(responseCallback) { /* The originally synchronous responseCallback can * be replaced with an asynchronous function, because * its return value is never saved or used anywhere. */ const originalResponseCallback = responseCallback; responseCallback = async function(data, status) { // Data is sometimes string and sometimes JSON let wasParsed = false; if (typeof data === "string") { try { let parsed = JSON.parse(data); wasParsed = true; data = parsed; } catch { wasParsed = false; } } if (typeof data === "object") { // If the request response contains a map array if (Object.keys(data).includes("maps")) { // Bonk 2 maps if (typeof data.maps === "object") { const NSFWList = await getNSFWList(); if (/^G/.test(read(pretty_top_level)) || await isOK([read(pretty_top_name)]) || pushOK(read(pretty_top_name)) ) { data.maps.ok = true; } for (let i = 0; i < data.maps.length; i++) { const map = data.maps[i]; const hash = await getHash(map.id.toString(), map.authorname, map.name); if (isNSFW(NSFWList, hash, map.authorname)) { addToNSFWList(map.id, map.authorname, map.name); } else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) { if (map.remixid > 0) { const rxhash = await getHash(map.remixid.toString(), map.remixauthor, map.remixname); if (isNSFW(NSFWList, rxhash, map.remixauthor)) { addToNSFWList(map.id, map.authorname, map.name); } } } } } // Bonk 1 maps else if (typeof data.maps === "string") { const NSFWList = await getNSFWList(); let maps = []; data.maps.split("&").forEach(m => { if(!m) return; let [prop, value, index, _] = m.split("="); if (prop === "cant") return; [_, prop, index] = [...prop.match("(.+?)([0-9]+)")]; if (prop === "mapname") { value = decodeURIComponent(value.replaceAll('+', '%20')); } maps[index] = maps[index] || {}; maps[index][prop] = value; }); if (/^G/.test(read(pretty_top_level)) || await isOK([read(pretty_top_name)]) || pushOK(read(pretty_top_name)) ) { maps.ok = true; } for (let i = 0; i < maps.length; i++) { const map = maps[i]; const hash = await getHash(map.mapid, map.authorname, map.mapname); if (isNSFW(NSFWList, hash, map.authorname)) { addToNSFWList(map.mapid, map.authorname, map.mapname); } } } } else if (Object.keys(data).includes("replays")) { // Make sure the list has loaded before we allow replays pass through await getNSFWList(); if (body.offset === 0) { global.replays = []; } global.replays = global.replays.concat(data.replays); } } if (wasParsed) { data = JSON.stringify(data); } // Call original response callback return originalResponseCallback.apply(this, arguments); } // Set our own function as the response callback return responseDone.call(this, responseCallback); } return response; } } async function isOK(map) { return new Promise(async resolve => { resolve(getOK().includes( await sha256(map[Object.keys(map).sort((a, b) => a.localeCompare(b))[0]]) )); }); } function isNSFW(list, hash, author) { return list.includes(hash) || (author && settings.AUTHOR_BLOCKLIST.includes(author)); } async function getNSFWList() { return new Promise(resolve => { if (Date.now() - global.cacheTime > settings.CACHE_DURATION * 1000) { global.cacheTime = Date.now(); window.$.get("https://gist.githubusercontent.com/Salama/c93f26e0468aa743453339c8c993adaa/raw?" + Date.now()).done(r => { let NSFWList = r.split("\n"); let version = parseInt(NSFWList.splice(0, 1)); if (version > NSFWLIST_VERSION) { alert("NSFW map blocker is outdated!"); // Prevent future requests global.cacheTime = Infinity; resolve([]); return; } global.NSFWList = NSFWList; }); } resolve(global.NSFWList); }); } async function sha256(text) { const encoder = new TextEncoder(); const data = encoder.encode(text); const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); return hashHex; } async function pushOK(props) { let current = getOK(); current.push(await sha256(props)); return window.localStorage.setItem("nsfwok", current.join("")); } function getOK() { let ok = window.localStorage.getItem("nsfwok"); if (!ok) { window.localStorage.setItem("nsfwok", ""); return []; } return [...ok.match(/.{64}/g)]; } function read(e) { return e.textContent; } async function getHash(first, second, third, fourth = isOK([second])) { return sha256( ["zV6IqJQDncyqGv7t2efz︁1jQQyOpRwO︀", await sha256(first), await sha256(second), await sha256(third), await fourth].join("") ); } function addBlurStyle() { let blurStyle = document.createElement("style"); blurStyle.innerHTML = ` /* Blur map preview in map selector */ .blurNSFW > .maploadwindowtextname { filter: blur(6px); } .blurNSFW > .maploadwindowtextauthor { filter: blur(4px); } .blurNSFW > img { filter: blur(12px); overflow: hidden; } /* Blur map preview in vote */ .blurNSFW > #newbonklobby_votewindow_maptitle { filter: blur(6px); } .blurNSFW > #newbonklobby_votewindow_mapauthor { filter: blur(6px); } /* Unblur map preview in map selector on mouse hover */ .hoverUnblurNSFW:hover > .maploadwindowtextname { filter: unset !important; } .hoverUnblurNSFW:hover > .maploadwindowtextauthor { filter: unset !important; } .hoverUnblurNSFW:hover > img { filter: unset !important; } /* Blur map preview in lobby */ .blurNSFW > #newbonklobby_maptext { filter: blur(6px); } .blurNSFW > #newbonklobby_mapauthortext { filter: blur(4px); } .blurNSFW > #newbonklobby_mappreviewcontainer { filter: blur(12px); } /* Unblur map preview in lobby on mouse hover */ .hoverUnblurNSFW > #newbonklobby_maptext:hover { filter: unset !important; } .hoverUnblurNSFW > #newbonklobby_mapauthortext:hover { filter: unset !important; } .hoverUnblurNSFW > #newbonklobby_mappreviewcontainer:hover { filter: unset !important; } /*Disable blurred map background in lobby */ .disableMapthumbBig > #newbonklobby_mapthumb_big { display: none !important; } /* Disable map */ .fullyTransparent { opacity: 0 !important; } `; document.head.appendChild(blurStyle); } (async () => { addBlurStyle(); // Hijack requests const jqGet = requestHandler(window.$.get); const jqPost = requestHandler(window.$.post); window.$.get = jqGet; window.$.post = jqPost; getNSFWList(); const mapObserver = new MutationObserver(mutations => { // Don't handle bonk 1 maps for now... // Using this due to uncertainty of dbv accuracy //if (read(document.getElementById("maploadtypedropdowntitle")).includes("BONK 1")) return; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (getFromNSFWList(node.map.m.dbid, node.map.m.a, node.map.m.n)) { if (settings.HIDE_MAPS_FROM_MAP_SELECTOR) { node.remove(); } else { node.classList.add("blurNSFW"); if (settings.BLUR_ONLY_MAP_PREVIEW) { node.getElementsByClassName("maploadwindowtextname")[0].style.filter = "unset"; node.getElementsByClassName("maploadwindowtextauthor")[0].style.filter = "unset"; } if (settings.UNBLUR_MAP_ON_MOUSE_HOVER) { node.classList.add("hoverUnblurNSFW"); } } } } } }); mapObserver.observe(document.getElementById("maploadwindowmapscontainer"), {childList: true}); // Replay section if (settings.DISABLE_REPLAYS) { document.getElementById("bgreplay").style.display = "none"; } // TODO fix replayIndex drifting when spamming next replay. // Possibly fixed now? let replayIndex = -1; let ignoreReplayChange = false; const replayObserver = new MutationObserver(async mutations => { if (settings.DISABLE_REPLAYS || !settings.HIDE_NSFW_REPLAYS) return; let author = ""; let name = ""; for (const mutation of mutations) { if (mutation.type === "childList") { author = read(mutation.target.children[2]); name = read(mutation.target.children[0]); } else if (mutation.type === "attributes") { /* We need to override the visibility status to another visibility type * to prevent bonk from periodically updating replay credits, which * messes up replayIndex */ if (mutation.target.style.visibility === "inherit") { mutation.target.style.visibility = "visible"; } } } if (!author && !name) return; if (!ignoreReplayChange) { replayIndex++; } ignoreReplayChange = false; if (!global.replays[replayIndex]) { return; } const NSFWList = await getNSFWList(); const hash = await getHash(global.replays[replayIndex].mapid.toString(), author, name); if (isNSFW(NSFWList, hash, author) || getFromNSFWList(global.replays[replayIndex].mapid) ) { addToNSFWList(global.replays[replayIndex].mapid); global.ignoreNextReport = true; document.getElementById("pretty_top_replay_report").click(); } }); replayObserver.observe(document.getElementById("pretty_top_replay_text"), {childList: true, attributes: true}); document.getElementById("pretty_top_replay_back").addEventListener("click", () => { replayIndex--; replayIndex = Math.max(replayIndex, 0); ignoreReplayChange = true; }, true); document.getElementById("pretty_top_replay_next").addEventListener("click", () => { replayIndex++; replayIndex = Math.min(replayIndex, global.replays.length - 1); ignoreReplayChange = true; }, true); document.getElementById("pretty_top_replay_report").addEventListener("click", () => { ignoreReplayChange = true; global.replays.splice(replayIndex, 1); }); })(); window.NSFWFilter = { wrap: () => { const gameLoadedWaiter = setInterval(async() => { if ( window.NSFWFilter.menuFunctions !== undefined && Object.keys(window.NSFWFilter.menuFunctions).length >= 27) { clearInterval(gameLoadedWaiter); } else return; for (const i of Object.keys(window.NSFWFilter.menuFunctions)) { if (typeof window.NSFWFilter.menuFunctions[i] !== "function") continue; const ogFunc = window.NSFWFilter.menuFunctions[i]; window.NSFWFilter.menuFunctions[i] = function() { switch (i) { case "recvMapSuggest": if(!settings.WARN_ABOUT_MAP_REQUESTS) break; const suggestion = arguments[0]; getHash(suggestion.m.dbid.toString(), suggestion.m.a, suggestion.m.n).then(async hash => { const NSFWList = await getNSFWList(); if (isNSFW(NSFWList, hash, suggestion.m.a)) { addToNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n); } else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) { if (suggestion.m.rxid > 0) { const rxhash = await getHash(suggestion.m.rxid.toString(), suggestion.m.rxa, suggestion.m.rxn); if (isNSFW(NSFWList, rxhash, suggestion.m.rxa)) { addToNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n); } } } if (getFromNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n)) { window.NSFWFilter.menuFunctions.showStatusMessage("* NSFW map request", "#ff0000", false); } }); break; case "setGameSettings": handleLobbyMap(arguments[0].map.m); break; } let response = ogFunc.apply(window.NSFWFilter.menuFunctions, arguments); return response; } } for (const i of Object.keys(window.NSFWFilter.toolFunctions.networkEngine)) { if (typeof window.NSFWFilter.toolFunctions.networkEngine[i] !== "function") continue; const ogFunc = window.NSFWFilter.toolFunctions.networkEngine[i]; window.NSFWFilter.toolFunctions.networkEngine[i] = function() { switch (i) { case "sendMapAdd": unblurLobby(); break; } let response = ogFunc.apply(window.NSFWFilter.toolFunctions.networkEngine, arguments); return response; } } window.NSFWFilter.toolFunctions.networkEngine.on("mapAdd", async map => { await handleLobbyMap(map.m); }); }, 50); }, checkReplay: async map => { if (!settings.INCLUDE_REMIXES_OF_NSFW_MAPS) return; const NSFWList = await getNSFWList(); const hash = await getHash(map.dbid.toString(), map.a, map.n); const rxhash = await getHash(map.rxid.toString(), map.rxa, map.rxn); if (isNSFW(NSFWList, hash, map.a) || isNSFW(NSFWList, rxhash, map.rxa)) { // No author or name //addToNSFWList(map.dbid, map.a, map.n); addToNSFWList(map.dbid); } } } function blurLobby() { if (settings.HIDE_GAME_ON_NSFW) { document.getElementById("newbonkgamecontainer").classList.add("disableMapthumbBig"); document.getElementById("gamerenderer").classList.add("fullyTransparent"); } if (settings.HIDE_MAPS_FROM_MAP_SELECTOR) { document.getElementById("newbonklobby_mappreviewcontainer").style.display = "none"; } else { document.getElementById("newbonklobby_settingsbox").classList.add("blurNSFW"); document.getElementById("newbonklobby_votewindow_thumbcontainer").classList.add("blurNSFW"); document.getElementById("newbonklobby_votewindow").classList.add("blurNSFW"); if (settings.UNBLUR_MAP_ON_MOUSE_HOVER) { document.getElementById("newbonklobby_settingsbox").classList.add("hoverUnblurNSFW"); } if (settings.BLUR_ONLY_MAP_PREVIEW) { document.getElementById("newbonklobby_maptext").style.filter = ""; document.getElementById("newbonklobby_mapauthortext").style.filter = ""; document.getElementById("newbonklobby_votewindow_maptitle").style.filter = ""; document.getElementById("newbonklobby_votewindow_mapauthor").style.filter = ""; } } } function unblurLobby() { document.getElementById("newbonkgamecontainer").classList.remove("disableMapthumbBig"); document.getElementById("newbonklobby_settingsbox").classList.remove("blurNSFW", "hoverUnblurNSFW"); document.getElementById("newbonklobby_votewindow_thumbcontainer").classList.remove("blurNSFW"); document.getElementById("newbonklobby_votewindow").classList.remove("blurNSFW"); document.getElementById("gamerenderer").classList.remove("fullyTransparent"); document.getElementById("newbonklobby_mappreviewcontainer").style.display = ""; document.getElementById("newbonklobby_maptext").style.filter = ""; document.getElementById("newbonklobby_mapauthortext").style.filter = ""; } async function handleLobbyMap(map) { const NSFWList = await getNSFWList(); const hash = await getHash(map.dbid.toString(), map.a, map.n); if (isNSFW(NSFWList, hash, map.a)) { addToNSFWList(map.dbid, map.a, map.n); } else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) { if (map.rxid > 0) { const rxhash = await getHash(map.rxid.toString(), map.rxa, map.rxn); if (isNSFW(NSFWList, rxhash, map.rxa)) { addToNSFWList(map.dbid, map.a, map.n); } } } if (getFromNSFWList(map.dbid, map.a, map.n)) { blurLobby(); } else { unblurLobby(); } } function injector(str) { let newStr = str; const menuRegex = newStr.match(/== 13\){...\(\);}}/)[0]; newStr = newStr.replace(menuRegex, menuRegex + "window.NSFWFilter.menuFunctions = this; window.NSFWFilter.wrap();"); const toolRegex = newStr.match(/=new [A-Za-z0-9\$_]{1,3}\(this,[A-Za-z0-9\$_]{1,3}\[0\]\[0\],[A-Za-z0-9\$_]{1,3}\[0\]\[1\]\);/); newStr = newStr.replace(toolRegex, toolRegex + "window.NSFWFilter.toolFunctions = this;"); const replayRegex = newStr.match(/if\(([A-Za-z0-9\$_]{1,3}\[[0-9]+\])[^\)]+? < 5 \|\| [^\)]+? > 30\)/); newStr = newStr.replace(replayRegex[0], `{ window.NSFWFilter.checkReplay(${replayRegex[1]}.startingState.mm); }` + replayRegex[0]); global.injected = true; return newStr; } if (!window.bonkCodeInjectors) window.bonkCodeInjectors = []; window.bonkCodeInjectors.push(bonkCode => { try { return injector(bonkCode); } catch (e) { if (settings.DISABLE_INJECTION_FAIL_WARNING) return; throw e; } }); if (window.bonkHUD) { const createLabel = text => { const label = document.createElement("label"); label.classList.add("bonkhud-settings-label"); label.innerHTML = text; label.style.marginRight = "5px"; label.style.display = "inline-block"; label.style.verticalAlign = "middle"; return label; } const addTextarea = (target, name, variable) => { const label = createLabel(name); label.style.verticalAlign = "top"; const input = document.createElement("textarea"); input.rows = settings[variable].length + 5; input.oninput = () => { settings[variable] = input.value.split(/[\n,]/).map(a => a.trim()); window.bonkHUD.saveModSetting(ind, settings); } input.value = settings[variable].join("\n"); const container = document.createElement("div"); container.id = "nsfw_settings_" + variable; container.appendChild(label); container.appendChild(input); target.appendChild(container); } const addCheckbox = (target, name, variable) => { const label = createLabel(name); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.style.display = "inline-block"; checkbox.style.verticalAlign = "middle"; checkbox.checked = settings[variable]; checkbox.oninput = () => { settings[variable] = checkbox.checked; window.bonkHUD.saveModSetting(ind, settings); } const container = document.createElement("div"); container.id = "nsfw_settings_" + variable; container.appendChild(label); container.appendChild(checkbox); target.appendChild(container); } let nsfwSettings = window.bonkHUD.generateSection(); guiSettings.settingsContent = nsfwSettings; const ind = window.bonkHUD.createMod("NSFW Filter", guiSettings); if (window.bonkHUD.getModSetting(ind) != null) { const savedSettings = window.bonkHUD.getModSetting(ind); for (const setting of Object.keys(savedSettings)) { settings[setting] = savedSettings[setting]; } } addCheckbox(nsfwSettings, "Hide NSFW replays", "HIDE_NSFW_REPLAYS"); addCheckbox(nsfwSettings, "Hide all replays", "DISABLE_REPLAYS"); addCheckbox(nsfwSettings, "Hide NSFW maps from map selector", "HIDE_MAPS_FROM_MAP_SELECTOR"); addCheckbox(nsfwSettings, "Black out nsfw while in game", "HIDE_GAME_ON_NSFW"); addCheckbox(nsfwSettings, "Blur only map preview", "BLUR_ONLY_MAP_PREVIEW"); addCheckbox(nsfwSettings, "Unblur on mouse hover", "UNBLUR_MAP_ON_MOUSE_HOVER"); addCheckbox(nsfwSettings, "Include remixes", "INCLUDE_REMIXES_OF_NSFW_MAPS"); addCheckbox(nsfwSettings, "Disable injection fail warning", "DISABLE_INJECTION_FAIL_WARNING"); addTextarea(nsfwSettings, "Author blocklist<br />(separate users with a comma or a newline)", "AUTHOR_BLOCKLIST") const clearCache = document.createElement("button"); clearCache.textContent = "Clear Cache"; clearCache.onclick = () => { global.cacheTime = 0; global.NSFWList = []; global.NSFWMaps = new Set(); } nsfwSettings.appendChild(clearCache); window.bonkHUD.updateStyleSettings(); }