Bonk NSFW map filter

Blocks NSFW bonk maps

目前為 2024-07-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Bonk NSFW map filter
// @description Blocks NSFW bonk maps
// @namespace   salama.xyz
// @author      Salama
// @version     0.2
// @match       https://bonk.io/gameframe-release.html
// @match       https://bonkisback.io/gameframe-release.html
// @grant       none
// ==/UserScript==

////////////////////////
////    SETTINGS    ////
////////////////////////

// No semicolons for ease of use
// true = yes
// false = no

// If false, maps will be blurred instead
const HIDE_MAPS_FROM_MAP_SELECTOR    = true

// If true, map details won't be blurred
const BLUR_ONLY_MAP_PREVIEW          = false

const UNBLUR_MAP_ON_MOUSE_HOVER      = false

const INCLUDE_REMIXES_OF_NSFW_MAPS   = true

// Blocklist is cached for 5 * 60 sec = 5 minutes
const CACHE_DURATION = 5 * 60

/* DEFAULT SETTINGS

const HIDE_MAPS_FROM_MAP_SELECTOR    = true

const BLUR_ONLY_MAP_PREVIEW          = false

const UNBLUR_MAP_ON_MOUSE_HOVER      = false

const INCLUDE_REMIXES_OF_NSFW_MAPS   = true

const CACHE_DURATION = 5 * 60

*/

////////////////////////
///       CODE      ////
////////////////////////

'use strict';

const NSFWLIST_VERSION = 0;

const global = {
	cacheTime: 0,
	NSFWList: [],
	NSFWMaps: new Set()
};

function requestHandler(original) {
	return function(url,body,success,type) {
		// 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")) {
						const NSFWList = await getNSFWList();

						// TODO finish checks
						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 sha256(
								["Amye9CHqRTs", await sha256(map.id.toString()), map.authorname].join("᠎")
							);

							if(await isOK(map)) continue;

							if (NSFWList.includes(hash)) {
								global.NSFWMaps.add(map.id);
							}
							else if (INCLUDE_REMIXES_OF_NSFW_MAPS) {
								if (map.remixid > 0) {
									const rxhash = await sha256(
										["Amye9CHqRTs", await sha256(map.remixid.toString()), map.rxa].join("᠎")
									);

									if (NSFWList.includes(rxhash)) {
										global.NSFWMaps.add(map.id);
									}
								}
							}
						}
					}
				}

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

async function getNSFWList() {
	return new Promise(resolve => {
		if (Date.now() - global.cacheTime > CACHE_DURATION) {
			window.$.get("https://gist.githubusercontent.com/Salama/c93f26e0468aa743453339c8c993adaa/raw").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;
}

function addBlurStyle() {
	let blurStyle = document.createElement("style");
	blurStyle.innerHTML = `
		.blurNSFW {
			overflow: hidden;
		}
		.blurNSFW > .maploadwindowtextname {
			filter: blur(6px);
		}
		.blurNSFW > .maploadwindowtextauthor {
			filter: blur(4px);
		}
		.blurNSFW > img {
			filter: blur(12px);
		}

		.hoverUnblurNSFW:hover > .maploadwindowtextname {
			filter: unset !important;
		}
		.hoverUnblurNSFW:hover > .maploadwindowtextauthor {
			filter: unset !important;
		}
		.hoverUnblurNSFW:hover > img {
			filter: unset !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 => {
		for (const mutation of mutations) {
			for (const node of mutation.addedNodes) {
				if (global.NSFWMaps.has(node.map.m.dbid)) {
					if (HIDE_MAPS_FROM_MAP_SELECTOR) {
						node.remove();
					}
					else {
						node.classList.add("blurNSFW");
						if (BLUR_ONLY_MAP_PREVIEW) {
							node.getElementsByClassName("maploadwindowtextname")[0].style.filter = "unset";
							node.getElementsByClassName("maploadwindowtextauthor")[0].style.filter = "unset";
						}
						if (UNBLUR_MAP_ON_MOUSE_HOVER) {
							node.classList.add("hoverUnblurNSFW");
						}
					}
				}
			}
		}
	});
	mapObserver.observe(document.getElementById("maploadwindowmapscontainer"), {childList: true});
})();