User:Andrybak/Scripts/Unsigned generator.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* global mw */

(function() {
	'use strict';

	const USERSCRIPT_NAME = 'Unsigned generator';
	const VERSION = 4;
	const locale = {
		portletText: "Unsigned gen", // short to keep the "Tools" menu narrow
		portletTooltip: "Generate template for unsigned discussion messages",
		template: "subst:Unsigned", // see [[Template:Unsigned]]
		templateIp: "subst:Unsigned IP", // see [[Template:Unsigned IP]]
		prefixText: "Unsigned wikitext: ",
		previewPrefix: "Unsigned preview: ",
		errorPrefix: "Error: ",
		copyButtonText: "Copy",
		errorTimestamp: "Cannot find the timestamp of the edit. Aborting.",
		errorAuthor: "Cannot find the author of the edit. Aborting.",
	};

	const LOG_PREFIX = `[${USERSCRIPT_NAME} v${VERSION}]:`;
	const USERSCRIPT_OUTPUT_ID = 'userscript-unsigned-generator';

	function error(...toLog) {
		console.error(LOG_PREFIX, ...toLog);
	}

	function warn(...toLog) {
		console.warn(LOG_PREFIX, ...toLog);
	}

	function info(...toLog) {
		console.info(LOG_PREFIX, ...toLog);
	}

	function debug(...toLog) {
		console.debug(LOG_PREFIX, ...toLog);
	}

	function notify(notificationMessage) {
		mw.notify(notificationMessage, {
			title: USERSCRIPT_NAME
		});
	}

	function errorAndNotify(errorMessage, exception) {
		error(errorMessage, exception);
		notify(errorMessage);
	}

	function createTimestampWikitext(timestamp) {
		// https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time_format_like_in_signatures
		return `\u007B\u007Bsubst:#time:H:i, j xg Y "(UTC)"|${timestamp}}}`;
	}

	function createWikitext(template, user, timestamp) {
		const timestampWikitext = createTimestampWikitext(timestamp);
		return `{{${template}|${user}|${timestampWikitext}}}`;
	}

	function createErrorSpan(errorMessage) {
		const errorSpan = document.createElement('span');
		errorSpan.style.color = 'maroon';
		const prefix = document.createElement('b');
		prefix.appendChild(document.createTextNode(locale.errorPrefix));
		errorSpan.appendChild(prefix);
		errorSpan.appendChild(document.createTextNode(errorMessage));
		return errorSpan;
	}

	function createPreview(wikitext) {
		const previewContainer = document.createElement('div');
		const previewPrefix = document.createElement('span');
		previewPrefix.appendChild(document.createTextNode(locale.previewPrefix));
		previewContainer.appendChild(previewPrefix);
		const previewProper = document.createElement('div');
		previewContainer.appendChild(previewProper);
		const query = {
			action: 'parse',
			prop: ['text'],
			pst: true, // PST = pre-save transform; this makes substitution work properly
			preview: true,
			disablelimitreport: true,
			disableeditsection: true,
			disablestylededuplication: true,

			text: wikitext,
			title: mw.config.get('wgPageName'),
		};
		const api = new mw.Api();
		api.get(query).then(
			response => {
				// debug('Q:', query);
				// debug('R:', response);
				// previewDiv.appendChild(createErrorSpan('example of an error message'));
				previewProper.innerHTML = response.parse.text['*'];
			},
			rejection => {
				previewContainer.appendChild(createErrorSpan(rejection));
			}
		);
		return previewContainer;
	}

	/*
	 * Adapted from [[User:Enterprisey/diff-permalink.js]]
	 * https://en.wikipedia.org/wiki/User:Enterprisey/diff-permalink.js
	 */
	function showWikitextAboveBodyContent(wikitext) {
		info(wikitext);
		const wikitextInput = document.createElement('input');
		wikitextInput.id = USERSCRIPT_OUTPUT_ID;
		wikitextInput.value = wikitext;
		wikitextInput.style.fontFamily = 'monospace';
		wikitextInput.setAttribute('size', wikitext.length);

		const copyButton = document.createElement('button');
		copyButton.textContent = locale.copyButtonText;
		copyButton.style.padding = '0.5em';
		copyButton.style.cursor = 'pointer';
		copyButton.style.marginLeft = '0.5em';
		copyButton.onclick = () => {
			document.getElementById(USERSCRIPT_OUTPUT_ID).select();
			document.execCommand('copy');
		};

		const container = document.createElement('div');
		container.appendChild(document.createTextNode(locale.prefixText));
		container.appendChild(wikitextInput);
		container.appendChild(copyButton);
		const preview = createPreview(wikitext);
		container.appendChild(preview);
		document.getElementById('bodyContent').prepend(container);
	}

	function runPortletOnDiff() {
		/*
		 * Reference documentation about keys and values in mw.config:
		 * https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config
		 */
		const diffTimestampElement = document.querySelector('#mw-diff-ntitle1 .mw-diff-timestamp');
		if (!diffTimestampElement) {
			errorAndNotify(locale.errorTimestamp);
			return;
		}
		const mwUserlink = document.querySelector('#mw-diff-ntitle2 .mw-userlink');
		if (!mwUserlink) {
			errorAndNotify(locale.errorAuthor);
			return;
		}
		const template = mwUserlink.classList.contains('mw-anonuserlink') ? locale.templateIp : locale.template;
		const usernameOrIp = mwUserlink.innerText;
		const isoTimestamp = diffTimestampElement.getAttribute('data-timestamp');
		const wikitext = createWikitext(template, usernameOrIp, isoTimestamp);
		showWikitextAboveBodyContent(wikitext);
	}

	function runPortletOnPermalink() {
		const params = new URLSearchParams(document.location.search);
		if (params.get('oldid') === null) {
			warn('No oldid in the URL. Bug in script?');
			return;
		}
		const title = mw.config.get('wgPageName');
		const revisionId = mw.config.get("wgRevisionId");
		const api = new mw.Api();
		const queryParams = {
			action: 'query',
			prop: 'revisions',
			rvprop: 'ids|user|timestamp',
			rvslots: 'main',
			formatversion: 2, // v2 has nicer field names in responses

			titles: title,
			rvstartid: revisionId,
			rvendid: revisionId,
		};
		api.get(queryParams).then(
			response => {
				// debug('Q:', queryParams);
				// debug('R:', response);
				const revision = response?.query?.pages[0]?.revisions[0];
				if (!revision) {
					errorAndNotify(`Cannot parse response to query ${queryParams} (getting ${revisionId} for page ${title}).`);
					return;
				}
				const template = revision.anon === true ? locale.templateIp : locale.template;
				const usernameOrIp = revision.user;
				const isoTimestamp = revision.timestamp;
				const wikitext = createWikitext(template, usernameOrIp, isoTimestamp);
				showWikitextAboveBodyContent(wikitext);
			},
			rejection => {
				errorAndNotify(`Cannot load revision ${revisionId} for page ${title}.`, rejection);
			}
		);
	}

	/*
	 * The main function of the script.
	 */
	function runPortlet() {
		if (mw.config.get('wgDiffNewId') === null && mw.config.get('wgDiffOldId') === null) {
			runPortletOnPermalink();
		} else {
			runPortletOnDiff();
		}
	}

	function wait(message) {
		info(message);
		setTimeout(lazyLoadUnsignedGenerator, 200);
	}

	/*
	 * Infrastructure to ensure the script can run.
	 */
	function lazyLoadUnsignedGenerator() {
		const params = new URLSearchParams(document.location.search);
		if (mw.config.get('wgDiffNewId') === null && mw.config.get('wgDiffOldId') === null && params.get('oldid') === null) {
			info('Not a diff or permalink view. Aborting.');
			return;
		}
		const namespaceId = mw.config.get('wgNamespaceNumber');
		if (namespaceId % 2 == 0 && namespaceId != 4) {
			// not a talk page and not project namespace
			info('Not a discussion namespace. Aborting.');
			return;
		}
		if (!mw.loader.using) {
			wait('Function mw.loader.using is no loaded yet. Waiting...');
			return;
		}
		debug('Loading...');
		mw.loader.using(
			['mediawiki.util'],
			() => {
				const link = mw.util.addPortletLink('p-cactions', '#', locale.portletText, 'ca-unsigned-generator', locale.portletTooltip);
				if (!link) {
					info('Cannot create portlet link (mw.util.addPortletLink). Assuming unsupported skin. Aborting.');
					return;
				}
				link.onclick = event => {
					mw.loader.using('mediawiki.api', runPortlet);
				};
			},
			(e) => {
				error('Cannot add portlet link', e);
			}
		);
	}

	if (document.readyState !== 'loading') {
		debug('document.readyState =', document.readyState);
		lazyLoadUnsignedGenerator();
	} else {
		warn('Cannot load yet. Setting up a listener...');
		document.addEventListener('DOMContentLoaded', lazyLoadUnsignedGenerator);
	}
})();