Greasy Fork is available in English.

4chan anonymize file names

Anonymizes file names when posting on 4chan

// ==UserScript==
// @name        4chan anonymize file names
// @namespace   a508vdvu3inxqz4zeagu
// @match       https://boards.4chan.org/*
// @match       https://boards.4channel.org/*
// @grant       none
// @version     1.1
// @description Anonymizes file names when posting on 4chan
// @inject-into content
// @run-at      document-start
// ==/UserScript==

(function () {
	"use strict";

	const { window, document, Set, String, Math, performance, MutationObserver } = globalThis;
	const File = window.File;


	// Changes the file name to a random plausible timestamp
	const genFileName = (oldName) => {
		const minute = 60 * 1_000_000;  // microseconds
		const year = minute * 60 * 24 * 365;
		const now = Math.floor((performance.timeOrigin + performance.now()) * 1000);

		// Random timestamp 1 minute to 1 year in the past
		let newName = String(now - Math.floor(Math.random() * (year - minute + 1) + minute));

		const dot = oldName.lastIndexOf(".");
		if (dot !== -1) {
			// Copy over file extension
			newName += oldName.substring(dot).toLowerCase();
		}

		return newName;
	};


	// Set up UI elements (2 paired checkboxes)
	const checkbox = document.createElement("input");
	const label = document.createElement("label");
	const checkboxQR = document.createElement("input");
	const labelQR = document.createElement("label");

	checkbox.type = checkboxQR.type = "checkbox";
	label.title = labelQR.title =  "Send actual filename when uploading";

	label.append(checkbox, " Filename");
	labelQR.append(checkboxQR, " Filename");


	// Turn off file name rewriting via the checkboxes
	let rewritingEnabled = true;

	const checkboxListener = function () {
		rewritingEnabled = !this.checked;

		// Transfer state to the other checkbox since it's global
		if (this === checkbox) {
			checkboxQR.checked = this.checked;
		} else {
			checkbox.checked = this.checked;
		}
	};

	checkbox.addEventListener("input", checkboxListener);
	checkboxQR.addEventListener("input", checkboxListener);


	// Watch for forms being serialized, and change file names.
	window.addEventListener("formdata", ({ formData }) => {
		// Use global var instead of unregistering/re-registering this listener.
		// Keeping it in place ensures we always run first.
		if (!rewritingEnabled) {
			return;
		}

		// Gather keys pointing to at least 1 file entry.
		const fileEntries = new Set();

		for (const [key, val] of formData) {
			if (val instanceof File) {
				fileEntries.add(key);
			}
		}

		// For each key...
		for (const key of fileEntries) {
			// Get all entry values
			const values = formData.getAll(key);

			// Remove them
			formData.delete(key);

			// Re-add them
			for (const val of values) {
				// ... with a new file name, if it is a File.
				if (val instanceof File) {
					formData.append(key, val, genFileName(val.name));
				} else {
					formData.append(key, val);
				}
			}
		}
	}, { capture: true, passive: true });


	const tryAddQuickReply = () => {
		if (!labelQR.isConnected) {
			document.getElementById("qrFile")?.after(" ", labelQR);
		}
	};


	// Insert post form checkbox on load
	window.addEventListener("DOMContentLoaded", () => {
		document.getElementById("postFile").after(" ", label);
		tryAddQuickReply();

		// Wait for quick reply form to show up
		new MutationObserver(tryAddQuickReply).observe(document.body, { childList: true });
	});
})();