// ==UserScript==
// @name Bonk NSFW map filter
// @namespace salama.xyz
// @author Salama
// @version 0.5
// @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
// ==/UserScript==
////////////////////////
//// SETTINGS ////
////////////////////////
/* Optionally depends on code injector
* 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
*/
// This script connects to GitHub to get an up to date list.
// No semicolons for ease of use
// true = yes
// false = no
const HIDE_NSFW_REPLAYS = true
const DISABLE_REPLAYS = false
// 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
/* You can enable this in case injection fails due to a
* bonk update. The mod will still work for its main purpose
*/
const DISABLE_INJECTION_FAIL_WARNING = false
const WARN_ABOUT_MAP_REQUESTS = true
// Blocklist is cached for 5 * 60 sec = 5 minutes
const CACHE_DURATION = 5 * 60
/* DEFAULT SETTINGS
const HIDE_NSFW_REPLAYS = true
const DISABLE_REPLAYS = false
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 DISABLE_INJECTION_FAIL_WARNING = false
const WARN_ABOUT_MAP_REQUESTS = true
const CACHE_DURATION = 5 * 60
*/
////////////////////////
/// CODE ////
////////////////////////
'use strict';
const NSFWLIST_VERSION = 0;
const global = {
cacheTime: 0,
NSFWList: [],
NSFWMaps: new Set(),
replays: [],
ignoreNextReport: false
};
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 (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")) {
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 getHash(map.id.toString(), map.authorname);
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 getHash(map.remixid.toString(), map.remixauthor);
if (NSFWList.includes(rxhash)) {
global.NSFWMaps.add(map.id);
}
}
}
}
}
else if (Object.keys(data).includes("replays")) {
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]])
));
});
}
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 getHash(first, second) {
return sha256(
["Amye9CHqRTs", await sha256(first), second].join("")
);
}
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 = `
/* Blur map preview in map selector */
.blurNSFW {
overflow: hidden;
}
.blurNSFW > .maploadwindowtextname {
filter: blur(6px);
}
.blurNSFW > .maploadwindowtextauthor {
filter: blur(4px);
}
.blurNSFW > img {
filter: blur(12px);
}
/* 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 => {
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});
// Replay section
if (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 (DISABLE_REPLAYS || !HIDE_NSFW_REPLAYS) return;
for (const mutation of mutations) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
const index = [...mutation.target.children].indexOf(node);
// Author
if (index === 2) {
if (!ignoreReplayChange) {
replayIndex++;
}
ignoreReplayChange = false;
if (!global.replays[replayIndex] ||
await isOK([read(node)])
) {
continue;
}
const NSFWList = await getNSFWList();
const hash = await sha256(
["Amye9CHqRTs", await sha256(global.replays[replayIndex].mapid.toString()), read(node)].join("")
);
if (NSFWList.includes(hash)) {
global.NSFWMaps.add(global.replays[replayIndex].mapid);
global.ignoreNextReport = true;
document.getElementById("pretty_top_replay_report").click();
}
}
}
}
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";
}
}
}
});
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(!WARN_ABOUT_MAP_REQUESTS) break;
const suggestion = arguments[0];
getHash(suggestion.m.dbid.toString(), suggestion.m.a).then(async hash => {
const NSFWList = await getNSFWList();
if (await isOK(suggestion.m)) return;
if (NSFWList.includes(hash)) {
global.NSFWMaps.add(suggestion.m.dbid);
}
else if (INCLUDE_REMIXES_OF_NSFW_MAPS) {
if (suggestion.m.rxid > 0) {
const rxhash = await getHash(suggestion.m.rxid.toString(), map.rxa);
if (NSFWList.includes(rxhash)) {
global.NSFWMaps.add(suggestion.m.dbid);
}
}
}
if (global.NSFWMaps.has(suggestion.m.dbid)) {
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);
}
}
function blurLobby() {
document.getElementById("newbonkgamecontainer").classList.add("disableMapthumbBig");
document.getElementById("gamerenderer").classList.add("fullyTransparent");
if (HIDE_MAPS_FROM_MAP_SELECTOR) {
document.getElementById("newbonklobby_mappreviewcontainer").style.display = "none";
}
else {
document.getElementById("newbonklobby_settingsbox").classList.add("blurNSFW");
if(UNBLUR_MAP_ON_MOUSE_HOVER) {
document.getElementById("newbonklobby_settingsbox").classList.add("hoverUnblurNSFW");
}
if(BLUR_ONLY_MAP_PREVIEW) {
document.getElementById("newbonklobby_maptext").style.filter = "";
document.getElementById("newbonklobby_mapauthortext").style.filter = "";
}
}
}
function unblurLobby() {
document.getElementById("newbonkgamecontainer").classList.remove("disableMapthumbBig");
document.getElementById("newbonklobby_settingsbox").classList.remove("blurNSFW", "hoverUnblurNSFW");
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 hash = await getHash(map.dbid.toString(), map.a);
const NSFWList = await getNSFWList();
if (await isOK(map)) {}
else if (NSFWList.includes(hash)) {
global.NSFWMaps.add(map.dbid);
}
else if (INCLUDE_REMIXES_OF_NSFW_MAPS) {
if (map.rxid > 0) {
const rxhash = await getHash(map.rxid.toString(), map.rxa);
if (NSFWList.includes(rxhash)) {
global.NSFWMaps.add(map.dbid);
}
}
}
if (global.NSFWMaps.has(map.dbid)) {
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;");
return newStr;
}
if(!window.bonkCodeInjectors) window.bonkCodeInjectors = [];
window.bonkCodeInjectors.push(bonkCode => {
try {
return injector(bonkCode);
}
catch (e) {
if (DISABLE_INJECTION_FAIL_WARNING) return;
throw e;
}
});