Sleazy Fork is available in English.

Ask for Permission

Helps you ask for permission to repost something from Fur Affinity

// ==UserScript==
// @name         Ask for Permission
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Helps you ask for permission to repost something from Fur Affinity
// @author       Mantikor
// @license      GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @match        https://www.furaffinity.net/*
// @icon         https://www.google.com/s2/favicons?domain=tampermonkey.net
// @grant        GM.addValueChangeListener
// @grant        GM.deleteValue
// @grant        GM.getValue
// @grant        GM.log
// @grant        GM.notification
// @grant        GM.openInTab
// @grant        GM.registerMenuCommand
// @grant        GM.setValue
// ==/UserScript==

// Notes about the header
// The match pattern https://www.furaffinity.net/* matches all pages on Fur Affinity

(async function() {
    'use strict';

    // --------------------------------------------------------------------------------
    // Notifications: Error, Success
    // --------------------------------------------------------------------------------
    const error_extracting_name_post = "Couldn't extract Post Title.";
    const error_extracting_name_recipient = "Couldn't extract Recipient Name.";
    const error_extracting_name_sender = "Couldn't extract Sender Name.\nYou are probably not logged in to Fur Affinity.";
    const error_extracting_url_newpm = "Couldn't extract Note URL.";
    const error_finding_message = "Couldn't find Message field.";
    const error_finding_subject = "Couldn't find Subject field.";
    const error_table_name_recipient_empty = "Recipient Name in row <row> is empty";
    const error_table_url_newpm_exists = "Table row <row> already contains Note URL:\n<url_newpm>";
    const error_table_url_newpm_invalid = "Note URL in row <row> is not valid:\n<url_newpm>";
    const success_settings_reset = "Settings reset.";
    const success_settings_saved = "Settings saved.";
    const success_user_information_copied = "User information copied to 'Ask User and Others' tab.";

    // --------------------------------------------------------------------------------
    // FA: newpm
    // --------------------------------------------------------------------------------
    // Extract the "message" textarea from a newpm page
    const newpm_extract_message = {css: "textarea[name='message']", func: e => e, min: 1, max: 1, index: 0, re: null};
    // Extract the "subject" input from a newpm page
    const newpm_extract_subject = {css: "input[name='subject']", func: e => e, min: 1, max: 1, index: 0, re: null};
    // Extract the system message
    // User Fender has declined to participate in the note system. You may attempt to find alternate means of contact listed on their userpage or in one of their journals.
    const newpm_extract_system_message = {css: "div.redirect-message", func: e => e, min: 1, max: 1, index: 0, re: null};
    // Check if we're on a newpm page; the trailing [^$]+ matches the query string (must be present to trigger AfP)
    // https://www.furaffinity.net/newpm/fender/
    const newpm_regexp = /^https:\/\/www\.furaffinity\.net\/newpm\/[^\/]+\/[^$]+$/;

    // --------------------------------------------------------------------------------
    // FA: user
    // --------------------------------------------------------------------------------
    // Extract name_recipient from a user page
    // <meta property="og:title" content="Userpage of Fender -- Fur Affinity [dot] net" />
    const user_extract_name_recipient = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^Userpage of (?<value>[^$]+) -- Fur Affinity \[dot\] net$/};
    // Extract url_newpm from a user page
    // <a class="button usernav-watch-flex-button hideondesktop" href="/newpm/fender/">Note</a>
    const user_extract_url_newpm = {css: "a[href*='newpm']", func: e => e.href, min: 1, max: 1, index: 0, re: null};
    // Check if we're on a user page; the trailing slash isn't required, because some links to user pages omit it
    // https://www.furaffinity.net/user/fender/
    const user_regexp_url = /^https:\/\/www\.furaffinity.net\/user\/[^$]+$/;

    // --------------------------------------------------------------------------------
    // FA: view
    // --------------------------------------------------------------------------------
    // Extract name_sender from a view page
    // <a href="/user/mantikor"><img class="loggedin_user_avatar menubar-icon-resize avatar" style="cursor:pointer" alt="Mantikor" src="//a.furaffinity.net/9999999999/mantikor.gif"/></a>
    const view_extract_name_sender = {css: "img.loggedin_user_avatar", func: e => e.alt, min: 2, max: 2, index: 0, re: null};
    // Extract name_post from a view page
    // <meta property="og:title" content="Fender (Character Sheet) by Fender" />
    const view_extract_name_post = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^(?<value>[^$]+) by [^$]+$/};
    // Extract name_recipient from a view page
    // <meta property="og:title" content="Fender (Character Sheet) by Fender" />
    const view_extract_name_recipient = {css: "meta[property='og:title']", func: e => e.content, min: 1, max: 1, index: 0, re: /^[^$]+ by (?<value>[^$]+)$/};
    // Extract url_newpm from a view page
    // <div class="note"><a href="/newpm/fender/">Note</a></div>
    const view_extract_url_newpm = {css: "a[href*='newpm']", func: e => e.href, min: 2, max: 2, index: 0, re: null};
    // Check if we're on a view page; the trailing slash isn't required, because some links to view pages omit it; the trailing [^$]* matches optional URL fragments (i.e. links to comments)
    // https://www.furaffinity.net/view/4483888/
    const view_regexp_url = /^(?<url>https:\/\/www\.furaffinity\.net\/view\/\d+)[^$]*$/;

    // --------------------------------------------------------------------------------
    // AfP: Ask User and Others
    // --------------------------------------------------------------------------------
    // Used to check if we're on the "Ask User and Others" page => Replace the 404 page; the trailing [^$]+ matches the query string (must be present to trigger AfP)
    // AfP uses a regexp literal instead of a constructed regexp, because the latter doesn't escape periods in the URL:
    // const regexp_ask_user_and_others = new RegExp(url_ask_user_and_others + "[^$]+");
    const ask_user_and_others_regexp_url = /^https:\/\/www\.furaffinity\.net\/userscripts\/ask-for-permission\/ask-user-and-others\/[^$]+$/;
    // Opened when the user clicks "Ask for Permission -> Ask User and Others"
    const ask_user_and_others_url = "https://www.furaffinity.net/userscripts/ask-for-permission/ask-user-and-others/";

    // --------------------------------------------------------------------------------
    // AfP: Settings
    // --------------------------------------------------------------------------------
    // Opened when the user clicks "Ask for Permission -> Settings"
    // Also: Used to check if we're on the "Settings" page => Replace the 404 page
    const settings_url = "https://www.furaffinity.net/userscripts/ask-for-permission/config/";

    // --------------------------------------------------------------------------------
    // Other constants
    // --------------------------------------------------------------------------------
    // AfP passes name_userscript in all query strings, so other userscripts that use query strings don't trigger AfP
    const name_userscript = "Ask for Permission";
    // Separates the last and second-to-last other recipient in message_ask_user_and_others
    const separator_and = " and ";
    // Separates the other recipients (except the last one) in message_ask_user_and_others
    const separator_comma = ", ";

    const template_settings = `
<!DOCTYPE html>
<html>
	<head>
		<title>Ask for Permission: Settings</title>
	</head>
	<body>
		<h1>Ask for Permission: Settings</h1>
		<p>
			<label for="subject">Subject</label>
			<br>
			<input type="text" id="subject" size="75">
			</textarea>
		</p>
		<p>
			<label for="message_ask_user_only">Message (Ask User Only)</label>
			<br>
			<textarea id="message_ask_user_only" rows=10 cols=75>
			</textarea>
		</p>
		<p>
			<label for="message_ask_user_and_others">Message (Ask User and Others)</label>
			<br>
			<textarea id="message_ask_user_and_others" rows=10 cols=75>
			</textarea>
		</p>
		<p>
			<button id="save_settings_button">Save Settings</button>
		</p>
		<p>
			<button id="reset_settings_button" disabled>Reset Settings</button>
			<input type="checkbox" id="reset_settings_checkbox">
			<label for="reset_settings_checkbox">Enable the "Reset Settings" button</label>
		</p>
	</body>
</html>`
.trim()

    const template_ask_user_and_others = `
<!DOCTYPE html>
<html>
	<head>
		<title>Ask for Permission: Ask User and Others</title>
	</head>
	<body>
	<h1>Ask for Permission: Ask User and Others</h1>
	<table id="name_table">
		<thead>
			<tr>
				<th scope="col">Note URL</th>
				<th scope="col">Recipient Name</th>
				<th scope="col"></th>
				<th scope="col"></th>
			</tr>
		</thead>
		<tbody>
		</tbody>
	</table>
	<p>
		<button id="create_messages_button">Create Messages</button>
	</p>
	<template id="table_row">
		<tr>
			<td>
				<input type="text" name="url_newpm" size="50">
			</td>
			<td>
				<input type="text" name="name_recipient" size="50">
			</td>
			<td>
				<button name="row_add">Add Row</button>
			</td>
			<td>
				<button name="row_delete">Delete Row</button>
			</td>
		</tr>
	</template>
	</body>
</html>
`.trim()

    const template_subject = "Uploading \"<name_post>\" to e621?";

    const template_message_ask_user_only = `
Hi <name_recipient>,

may I upload <url_post> to https://e621.net/posts ?

--
<name_sender>
`.trim();

    const template_message_ask_user_and_others = `
Hi <name_recipient>,

may I upload <url_post> to https://e621.net/posts ?
I'll also ask <names_recipients_other>.

--
<name_sender>
`.trim();

    function capitalizeFirstLetter(text) {
        return text.substr(0, 1).toUpperCase() + text.substr(1);
    }

    function createElement(tag_name, attributes, text_content) {
        let element = document.createElement(tag_name);

        for (const key in attributes) {
            element.setAttribute(key, attributes[key]);
        }

        if (text_content) {
            element.appendChild(document.createTextNode(text_content));
        }

        return element;
    }

    function extract(doc, info) {
        let elements = doc.querySelectorAll(info.css);

        if ((elements.length < info.min) || (elements.length > info.max)) {
            return null;
        }

        let text = info.func(elements[info.index]);
        if (! info.re) {
            return text;
        }
        if (! info.re.test(text)) {
            return null;
        }

        return text.match(info.re).groups.value;
    }

    function queryString(url, data) {
        let query_string = new URL(url);

        for (const [key, value] of Object.entries(data)) {
            query_string.searchParams.set(key, value);
        }

        return query_string.href;
    }

    function replaceDocument(doc, html) {
        let newDoc = new DOMParser().parseFromString(html, "text/html");
        ["head", "body"].forEach(element_name => {
            doc[element_name].remove();
            doc.documentElement.append(newDoc[element_name]);
        })
    }

    // Ask User and Others:
    // Add a new row to the table
    // Returns true if the new row was successfully added to the table, false otherwise
    function tableAddRow(table, index, url_newpm, name_recipient, row_delete) {
        let tbody = table.tBodies[0];

        if (url_newpm) {
            // Check if there's already a row containing url_newpm
            for (const row of tbody.rows) {
                let inputs = row.querySelectorAll("input");
                if (inputs[0].value == url_newpm) {
                    GM.notification({text: error_table_url_newpm_exists.replaceAll("<row>", row.rowIndex).replaceAll("<url_newpm>", url_newpm)});
                    return false;
                }
            }
        }

        // Create the new row
        let table_row = document.getElementById("table_row").content.firstElementChild.cloneNode(true);
        let inputs = table_row.querySelectorAll("input");
        inputs[0].value = url_newpm ? url_newpm : "";
        inputs[1].value = name_recipient ? name_recipient: "";
        if (! row_delete) {
            // Remove the "Delete Row" button
            let buttons = table_row.querySelectorAll("button");
            buttons[1].remove();
        }
        tbody.insertBefore(table_row, tbody.rows[index]);

        return true;
    }

    function ask(ask_others) {
        let name_post;
        let name_recipient;
        let name_sender;
        let url_newpm;
        let url_post;
        let url_tab;

        let elements;

        // url_post
        url_post = document.URL.match(view_regexp_url).groups.url;

        // name_sender
        name_sender = extract(document, view_extract_name_sender);
        if (! name_sender) {
            GM.notification({text: error_extracting_name_sender});
            return;
        }
        name_sender = capitalizeFirstLetter(name_sender);

        // name_post
        name_post = extract(document, view_extract_name_post);
        if (! name_post) {
            GM.notification({text: error_extracting_name_post});
            return;
        }
        // name_recipient
        name_recipient = extract(document, view_extract_name_recipient);
        if (! name_recipient) {
            GM.notification({text: error_extracting_name_recipient});
            return;
        }
        name_recipient = capitalizeFirstLetter(name_recipient);

        // url_newpm
        url_newpm = extract(document, view_extract_url_newpm);
        if (! url_newpm) {
            GM.notification({text: error_extracting_url_newpm});
            return;
        }

        if (ask_others) {
            url_tab = queryString(ask_user_and_others_url, {name_userscript: name_userscript, name_post: name_post, name_recipient: name_recipient, url_post: url_post, url_newpm: url_newpm, name_sender: name_sender});
        }
        else {
            url_tab = queryString(url_newpm, {name_userscript: name_userscript, name_post: name_post, name_recipient: name_recipient, url_post: url_post, names_recipients_other: "", name_sender: name_sender});
        }

        GM.openInTab(url_tab, {active: true, insert: true, setParent: true});
    }

    function askUserOnly() {
        ask(false);
    }

    function askUserAndOthers() {
        ask(true);
    }

    async function copyUserInformation() {
        let name_recipient;
        let url_newpm;
        let elements;

        // Extract name_recipient
        name_recipient = extract(document, user_extract_name_recipient);
        if (! name_recipient) {
            GM.notification({text: error_extracting_name_recipient});
            return;
        }
        name_recipient = capitalizeFirstLetter(name_recipient);

        // Extract url_newpm
        url_newpm = extract(document, user_extract_url_newpm);
        if (! url_newpm) {
            GM.notification({text: error_extracting_url_newpm});
            return;
        }

        // Send url_newpm and name_recipient to the "Ask User and Others" page
        //await GM.deleteValue("data_ask_user_and_others");
        await GM.setValue("data_ask_user_and_others", {url_newpm: url_newpm, name_recipient: name_recipient});

        // Close active tab:
        // Not possible with an XMonkey userscript
    }

    async function fillSettingsFields() {
        let [subject, message_ask_user_only, message_ask_user_and_others] = await Promise.all([
            GM.getValue("subject", template_subject),
            GM.getValue("message_ask_user_only", template_message_ask_user_only),
            GM.getValue("message_ask_user_and_others", template_message_ask_user_and_others)
        ]);
        document.getElementById("subject").value = subject;
        document.getElementById("message_ask_user_only").value = message_ask_user_only;
        document.getElementById("message_ask_user_and_others").value = message_ask_user_and_others;
    }

    function settings() {
        GM.openInTab(settings_url, {active: true, insert: true, setParent: true});
    }

    // --------------------------------------------------------------------------------
    // Settings
    // --------------------------------------------------------------------------------
    if (document.URL == settings_url) {
        // Replace the 404 page with the "Settings" page
        replaceDocument(document, template_settings);
        // Fill Subject and Messages
        fillSettingsFields();
        // Add event listeners
        // -- Save Settings button
        document.getElementById("save_settings_button").addEventListener("click", async function(event) {
            await Promise.all([
                GM.setValue("subject", document.getElementById("subject").value),
                GM.setValue("message_ask_user_only", document.getElementById("message_ask_user_only").value),
                GM.setValue("message_ask_user_and_others", document.getElementById("message_ask_user_and_others").value)
            ]);
            GM.notification({text: success_settings_saved});
        });
        // -- Reset Settings button
        document.getElementById("reset_settings_button").addEventListener("click", async function(event) {
            await Promise.all(["subject", "message_ask_user_only", "message_ask_user_and_others"].map(key => GM.deleteValue(key)));
            fillSettingsFields();
            GM.notification({text: success_settings_reset});

            document.getElementById("reset_settings_checkbox").checked = false;
            document.getElementById("reset_settings_button").disabled = true;
        });
        // -- Reset Settings checkbox
        document.getElementById("reset_settings_checkbox").addEventListener("change", async function(event) {
            document.getElementById("reset_settings_button").disabled = !event.target.checked;
        });
    }
    // --------------------------------------------------------------------------------
    // Ask User and Others
    // --------------------------------------------------------------------------------
    else if (ask_user_and_others_regexp_url.test(document.URL)) {
        GM.registerMenuCommand("Settings", settings);

        // Replace the 404 page with the "Ask User and Others" page
        replaceDocument(document, template_ask_user_and_others);

        // Extract parameters
        let params = new URL(document.URL).searchParams;

        // Fill the first row in the table
        let table = document.getElementById("name_table");
        tableAddRow(table, 0, params.get("url_newpm"), params.get("name_recipient"), false);

        // Add event listeners
        // -- GM Value Change
        GM.addValueChangeListener("data_ask_user_and_others", async function(name, old_value, new_value, remote) {
            if (remote) {
                // Add a new row at the bottom of the table
                let row_added = tableAddRow(table, -1, new_value.url_newpm, new_value.name_recipient, true);
                if (row_added) {
                    GM.notification({text: success_user_information_copied});
                }
                await GM.deleteValue("data_ask_user_and_others");
            }
        });
        // -- Table
        table.addEventListener("click", function(event) {
            switch (event.target.name) {
                case "row_add":
                    tableAddRow(table, event.target.parentElement.parentElement.rowIndex, null, null, true);
                    break;
                case "row_delete":
                    table.deleteRow(event.target.parentElement.parentElement.rowIndex);
                    break;
            }
        });
        // -- Create Messages button
        document.getElementById("create_messages_button").addEventListener("click", function(event) {
            let recipient_info = [];

            // Create a list of recipient information
            for (const row of table.tBodies[0].rows) {
                let inputs = row.querySelectorAll("input");
                recipient_info.push({url_newpm: inputs[0].value, name_recipient: inputs[1].value});
            }

            // Check if every recipient information is valid
            for (let i = 0; i < recipient_info.length; i++) {
                // Check url_newpm
                try {
                    let url = new URL(recipient_info[i].url_newpm);
                }
                catch (error) {
                    GM.notification({text: error_table_url_newpm_invalid.replaceAll("<row>", i + 1).replaceAll("<url_newpm>", recipient_info[i].url_newpm)});
                    return;
                }
                // Check name_recipient
                if (recipient_info[i].name_recipient == "") {
                    GM.notification({text: error_table_name_recipient_empty.replaceAll("<row>", i + 1)});
                    return;
                }
            }

            // Create Notes
            recipient_info.forEach(function(item, index, array) {
                let names_recipients_other = recipient_info.map(x => x.name_recipient).filter((filter_item, filter_index) => filter_index != index);
                let url_tab;

                if (names_recipients_other.length == 0) {
                    url_tab = queryString(item.url_newpm, {name_userscript: params.get("name_userscript"), name_post: params.get("name_post"), name_recipient: item.name_recipient, url_post: params.get("url_post"), names_recipients_other: "", name_sender: params.get("name_sender")});
                }
                else {
                    let last_element = names_recipients_other.pop();
                    if (names_recipients_other.length == 0) {
                        names_recipients_other = last_element;
                    }
                    else {
                        names_recipients_other = names_recipients_other.join(separator_comma) + separator_and + last_element;
                    }
                    url_tab = queryString(item.url_newpm, {name_userscript: params.get("name_userscript"), name_post: params.get("name_post"), name_recipient: item.name_recipient, url_post: params.get("url_post"), names_recipients_other: names_recipients_other, name_sender: params.get("name_sender")});
                }

                GM.openInTab(url_tab, {active: true, insert: true, setParent: true});
            });
        });
    }
    // --------------------------------------------------------------------------------
    // User
    // --------------------------------------------------------------------------------
    else if (user_regexp_url.test(document.URL)) {
        GM.registerMenuCommand("Copy User Information", copyUserInformation);
        GM.registerMenuCommand("Settings", settings);
    }
    // --------------------------------------------------------------------------------
    // View
    // --------------------------------------------------------------------------------
    else if (view_regexp_url.test(document.URL)) {
        GM.registerMenuCommand("Ask User Only", askUserOnly);
        GM.registerMenuCommand("Ask User and Others", askUserAndOthers);
        GM.registerMenuCommand("Settings", settings);
    }
    // --------------------------------------------------------------------------------
    // New PM
    // --------------------------------------------------------------------------------
    else if (newpm_regexp.test(document.URL)) {
        GM.registerMenuCommand("Settings", settings);

        // Check if the user has disabled notes
        let system_message = extract(document, newpm_extract_system_message);
        if (system_message) {
            return;
        }

        // Extract parameters
        let params = new URL(document.URL).searchParams;
        if (params.get("name_userscript") != name_userscript) {
            // AfP didn't open this newpm page
            return;
        }

        // Subject
        let subject = await GM.getValue("subject", template_subject);
        subject = subject.replaceAll("<name_post>", params.get("name_post"));

        let element_subject = extract(document, newpm_extract_subject);
        if (! element_subject) {
            GM.notification({text: error_finding_subject});
            return;
        }
        element_subject.value = subject;

        // Message
        let message;
        if (params.get("names_recipients_other") == "") {
            message = await GM.getValue("message_ask_user_only", template_message_ask_user_only);
        }
        else {
            message = await GM.getValue("message_ask_user_and_others", template_message_ask_user_and_others);
        }

        for (const element of ["name_recipient", "url_post", "names_recipients_other", "name_sender"]) {
            message = message.replaceAll("<" + element + ">", params.get(element));
        }

        let element_message = extract(document, newpm_extract_message);
        if (! element_message) {
            GM.notification({text: error_finding_message});
            return;
        }
        element_message.value = message;
    }
    else {
        GM.registerMenuCommand("Settings", settings);
    }
})();