🏠 Home 

FA Blacklist

Adds a blacklist to Fur Affinity. Also adds the ability to replace typed terms with other terms. If installed correctly you should see a link titled "Edit Blacklist" below the search box on FA's search page.

// ==UserScript==
// @name         FA Blacklist
// @namespace    https://greasyfork.org/en/scripts/453103-fa-blacklist
// @version      1.2.1
// @description  Adds a blacklist to Fur Affinity. Also adds the ability to replace typed terms with other terms. If installed correctly you should see a link titled "Edit Blacklist" below the search box on FA's search page.
// @author       Nin
// @license      GNU GPLv3
// @match        https://www.furaffinity.net/*
// @icon         https://www.google.com/s2/favicons?domain=furaffinity.net
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// ==/UserScript==
// How many tabs to add between the blacklist and search
var tabBuffer = "\t".repeat(30);
// Save a script setting
function saveUserData(key, value) {
'use strict';
GM_setValue(key, JSON.stringify(value));
}
// Load a script setting
async function loadUserData(key, defaultValue) {
'use strict';
let data = await GM_getValue(key);
if (data === undefined) {
return defaultValue;
}
return JSON.parse(data);
}
// Add extra search settings for using this script to FA's Search Settings section
function generateSearchSettings(blacklist, replace, block) {
'use strict';
if (!window.location.pathname.startsWith("/controls/site-settings/")){
return;
}
let replaceList = [];
for (const property in replace) {
replaceList.push(property + "=" + replace[property]);
}
let blockList = [];
for (const property in block) {
if (block[property] === "") {
blockList.push(property);
} else {
blockList.push(property + "=" + block[property]);
}
}
const blockString = blockList.join(", ");
prependSearchSetting(
"Block Offensive Text",
"A comma separated list of words you would like to automatically delete in all text that appears on FA. In the format:" +
"<br><i><span style='color:darkgray'>these, words, are, offensive</span></i><br>" +
"This is for words you might find offensive or dislike, but which you don't want to actually block as tags. <br/><br/>" +
"Advanced usage:" +
"<br><i><span style='color:darkgray'>delete, these, words, transexual=transgender, herm=dualsex</span></i><br>" +
"You can provide a replacement word using an equals sign. This helps maintain the flow of text in stories.",
blockString, "block", "Comma separated list...");
const replaceString = replaceList.join(", ");
prependSearchSetting(
"Find and Replace",
"A comma separated list of search terms to replace. In the format: <br><i><span style='color:darkgray'>term1=replacement1, term2=replacement2, tf=transformation</span></i><br> Replacements can contain advanced FA queries: <br><i><span style='color:darkgray'>noodles=(dragons|snakes), ramen=(snakes&soup)</span></i>",
replaceString, "replace", "Comma separated list...");
const blacklistString = blacklist.join(", ");
prependSearchSetting(
"Blacklist",
"A comma separated list of words to blacklist. In the format: <br><i><span style='color:darkgray'>these, are, search, terms, I, dislike</span></i>",
blacklistString, "blacklist", "Comma separated list...");
const saveButton = document.getElementsByName("save_settings")[0];
saveButton.addEventListener("click", function(){
let blacklist = document.getElementById("blacklist").value.replaceAll(/\s/g, "").split(",");
let replaceList = document.getElementById("replace").value.replaceAll(/\s/g, "").split(",");
let blockList = document.getElementById("block").value.replaceAll(/\s/g, "").split(",");
blacklist = blacklist.filter((word) => word.length > 0);
let replace = {};
for (const replaceText of replaceList) {
const split = replaceText.split("=");
if (split[0].length > 0) {
replace[split[0]] = split[1];
}
}
let block = {};
for (const blockText of blockList) {
const split = (blockText + "=").split("=");
if (split[0].length > 0) {
block[split[0]] = split[1];
}
}
saveUserData("blacklist", blacklist);
saveUserData("replace", replace);
saveUserData("block", block);
});
}
// Add a search setting option to the start of the list of search settings on the Global Site Settings page
function prependSearchSetting(title, description, data, id, placeholder) {
'use strict';
let html = `
<div class="control-panel-item-container">
<div class="control-panel-item-name">
<h4>${title}</h4>
</div>
<div class="control-panel-item-description">${description}</div>
<div class="control-panel-item-options">
<div class="select-dropdown">
<input type="text" id="${id}" value="${data}" placeholder="${placeholder}" class="textbox" autocomplete="off" style="width: 100%;">
</div>
</div>
</div>`;
const element = document.getElementsByClassName("section-body")[2];
element.innerHTML = html + element.innerHTML;
}
// Add a link to edit the blacklist to search pages
function addSearchSettingsLink(){
'use strict';
if (!window.location.pathname.startsWith("/search/")){
return;
}
const searchBox = document.getElementsByClassName("browser-sidebar-search-box")[0];
searchBox.outerHTML += "<a href='https://www.furaffinity.net/controls/site-settings/#blacklist' style='color:darkgray;float:right'>Edit Blacklist</a>";
const ratingSection = document.getElementsByClassName("gridContainer")[1];
ratingSection.innerHTML += '<div class="gridContainer__item"><label><input type="checkbox" id="disable-blacklist"> Blacklisted </label><br></div>';
}
// Remove the added query text from the query inputs on page load
function cleanInput() {
'use strict';
document.getElementsByName("q").forEach(function(input){
if (input.value !== "") {
console.log('Actual Search:\n' + input.value.replaceAll("\t", ""));
}
if (input.value.includes("\t")) {
input.value = input.value.substring(0, input.value.indexOf("\t"));
}
// Remove any sent zero width spaces
input.value = input.value.replaceAll("\u200B", "");
});
}
// Remove the blacklist text from FA's list of tags you searched
function cleanQueryStats(blacklist) {
'use strict';
if (document.getElementById("query-stats") !== null) {
var queryStats = document.getElementById("query-stats").children;
while (queryStats.length > 0 && blacklist.includes(queryStats[queryStats.length - 1].children[0].children[0].innerHTML)) {
queryStats[queryStats.length - 1].remove();
}
}
}
// Replace keywords in the query string according to the specified replacements
function replaceKeywords(replace) {
'use strict';
document.getElementsByName("q").forEach(function(input){
let append = "";
for (const property in replace) {
const pos_regex = new RegExp('(?<![-\u200B])\\b' + property + '\\b(?!\u200B)', "gi");
const neg_regex = new RegExp('(?<!\u200B)-\\b' + property + '\\b(?!\u200B)', "gi");
let pos_found = input.value.match(pos_regex);
if (pos_found !== null) {
for (const r###lt of pos_found) {
append += " " + replace[property];
}
}
let neg_found = input.value.match(neg_regex);
if (neg_found !== null) {
for (const r###lt of neg_found) {
append += " -(" + replace[property] + ")";
}
}
// Insert a zero width space between each replaced character so FA ignores it
input.value = input.value.replaceAll(pos_regex, [...property].join("\u200B"));
input.value = input.value.replaceAll(neg_regex, ["-", ...property].join("\u200B"));
}
input.value += append;
});
}
// Adds the blacklist text to the end of all query forms
function addBlacklist(blacklist) {
'use strict';
if (document.getElementById("disable-blacklist") === null || !document.getElementById("disable-blacklist").checked) {
document.getElementsByName("q").forEach(function(input){
if (blacklist.length > 0 && input.value.match(/ -bl\b/) === null){
input.value += " -" + blacklist.join(" -");
if (input.value.endsWith("-")) {
input.value = input.value.substring(0, input.value.length - 1);
}
}
});
}
}
// Adds a buffer of tabs to hide the added query text
function addBuffer() {
'use strict';
document.getElementsByName("q").forEach(function(input){
input.value += tabBuffer;
});
}
// Adds an onsubmit trigger for the element with the given ID to add the blacklist
function attachHandlers(elementID, blacklist, replace) {
'use strict';
var element = document.getElementById(elementID);
if (element !== null) {
element.addEventListener("submit", function(){
addBuffer();
replaceKeywords(replace);
addBlacklist(blacklist);
});
}
}
// Return some URI text that can be appended to a URI to add the blacklist
function blacklistURI(blacklist) {
'use strict';
let r###lt = tabBuffer + "-" + blacklist.join(" -");
if (r###lt.endsWith("-")) {
r###lt = r###lt.substring(0, r###lt.length - 1);
}
return encodeURIComponent(r###lt);
}
// If we somehow end up searching without the blacklist, redirect to add the blacklist
function redirect(blacklist) {
'use strict';
if ((!window.location.pathname.startsWith("/search/"))
|| (window.location.pathname === "/search/" && window.location.search === "")
|| window.location.pathname.includes("09%")
|| window.location.search.includes("09%")){
return;
}
console.log("Redirecting to add blacklist...");
window.location.href = window.location.href + blacklistURI(blacklist);
}
// Update the links on the tags of images to add the blacklist
function updateTags(blacklist) {
'use strict';
if (!window.location.pathname.startsWith("/view/")){
return;
}
[...document.getElementsByClassName("tags")].forEach(function(tag){
tag.firstChild.href += blacklistURI(blacklist);
});
}
// Recursively replace text in the given element and all sub-elements
function replaceText(element, pattern, replacement) {
'use strict';
for (let node of element.childNodes) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
case Node.DOCUMENT_NODE:
replaceText(node, pattern, replacement);
break;
case Node.TEXT_NODE:
node.textContent = node.textContent.replaceAll(pattern, replacement);
break;
}
}
}
// Delete tags with the given name from the tags list
function deleteTag(tagName) {
'use strict';
document.querySelectorAll(".tags a").forEach((element) => {
if (element.textContent.toLowerCase() === tagName.toLowerCase()) {
element.parentNode.remove()
}
});
}
// Delete all tags with the given name and replace text with the given replacement
function blockText(replacements) {
'use strict';
if (window.location.pathname.startsWith("/controls/site-settings/")){
return;
}
for (const text in replacements) {
deleteTag(text);
replaceText(document, new RegExp("\\b" + text.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') + "\\b", "gi"), replacements[text]);
}
}
async function main() {
'use strict';
const blacklist = loadUserData("blacklist", []);
const replace = loadUserData("replace", {});
const block = loadUserData("block", {});
addSearchSettingsLink();
cleanInput();
cleanQueryStats(await blacklist);
attachHandlers("search-form", await blacklist, await replace);
attachHandlers("searchbox", await blacklist, await replace);
generateSearchSettings(await blacklist, await replace, await block);
redirect(await blacklist);
updateTags(await blacklist);
blockText(await block);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}