Bonk NSFW map filter

Blocks NSFW bonk maps

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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