User:Suffusion of Yellow/mark-reverted.js

/* * mark-reverted.js * * Highlights diffs and permalinks by status: live, reverted, or unknown. * Should work on any page. Based on revision SHA1 only. */ // (function {	/* globals $, mw */	'use strict';

const MESSAGES = { 'mr-activate-text' : "Mark reverted", 'mr-activate-title' : "Highlight links by status (reverted, live, or unknown)", 'mr-link-unknown-title' : "This status of this edit could not be determined", 'mr-link-reverted-title' : "This edit has been reverted at least once", 'mr-link-live-title' : "This edit has identical text with the current revision", 'mr-link-error-title' : "An error occured while determining the status of this edit (see browser console)", 'mr-disallow-toomany': "There are $1 unique pages linked from here. Try again on a page with fewer links.", 'mr-warn-toomany' : "There are $1 unique pages linked from here. Continue?", };

const WINDOW_SIZE = 30; // Always look AT LEAST this far ahead/back

/*	 * It's possible for there to be 5000 unique titles linked from * one user contributions page. That would be 10000 API requests! * Set some sensible limits. */	const SOFT_PAGE_LIMIT = 100; // Prompt first const HARD_PAGE_LIMIT = 1000; // Nope const MAX_CONCURRENT_REQUESTS = 10;

const CSS_PAGE = "https://en.wikipedia.org/w/index.php?title=User:Suffusion_of_Yellow/mark-reverted.css&action=raw&ctype=text/css";

const API_USER_AGENT = "mark-reverted/0.1 (https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/mark-reverted.js)";

var api;

/*	 * Silently ignore API errors, but log them to the console. * We are making LOTS of requests and there's no need to panic * if one goes missing. Results will be "good enough". */	function handleApiError(code, details) { if (typeof code != 'string') throw code; // Something went very wrong

if (code == "http" && details.textStatus == "abort") return; // Aborted by user, not an error

console.log((code == "http") ?					"HTTP error: " + details.textStatus :					"API returned error \"" + code + "\": " + details.error.info); }

/*	 * Get a batch of revisions and mark all revisions as	 * "live" if the rev_sha1 is same as the current rev_sha1 * "reverted" if: *  Some later revision has a rev_sha1, S	 *   AND some earlier revision has the same rev_sha1, S	 *   AND the revision itself does NOT have rev_sha1 S	 * "unknown" otherwise */	async function getRevisions(state, revid, dir) { let page = state.page, revlist = state.revlist; let revmap = state.revmap, shamap = state.shamap; let start = revid, idx = revmap.get(revid);

if (idx !== undefined) { if (dir == "newer" ||				revlist.length - idx > WINDOW_SIZE ||				revlist[revlist.length - 1].parentid === 0) return; // Fully cached

start = revlist[revlist.length - 1].revid; // Partly cached }

let r = await api.get( {			action : 'query',			prop : 'revisions',			pageids : page.pageid,			rvprop : "ids|sha1",			rvstartid : start,			rvdir : dir,			rvlimit : WINDOW_SIZE		}).catch(handleApiError);

let revisions; try { revisions = r.query.pages[page.pageid].revisions;

if (dir == "newer") revisions.reverse; } catch(e) { return; }

for (let rev of revisions) { if (revmap.get(rev.revid)) continue;

rev.status = "unknown"; revmap.set(rev.revid, revlist.length); revlist.push(rev);

if (rev.sha1 !== undefined) { if (revlist[0].revid == page.lastrevid &&					rev.sha1 == revlist[0].sha1) rev.status = "live";

let last = shamap.get(rev.sha1);

if (last !== undefined) for (let j = last; j < revlist.length - 1; j++) if (revlist[j].status == "unknown" &&							revlist[j].sha1 !== rev.sha1) revlist[j].status = "reverted";

shamap.set(rev.sha1, revlist.length - 1); }		}	}

/*	 * Mark all links for a given page. * NOT concurrent; makes caching tricky */	async function markAllForPage(page, links) { let state = { page : page, revlist : [], revmap : new Map, shamap : new Map };

for (let rev of page.revisions) { await getRevisions(state, rev.revid, "newer"); await getRevisions(state, rev.revid, "older"); }

for (let rev of page.revisions) { let r = state.revmap.get(rev.revid); let result = r !== undefined ? state.revlist[r].status : "error";

links.get(rev.revid).addClass("mr-" + result); links.get(rev.revid).prop("title",									 mw.msg("mr-link-" + result + "-title")); }	}

/*	 * Concurrently mark all links for all pages */	async function markAll(pages, links) { let pending = [];

for(let [id, page] of pages) { let idx = pending.length < MAX_CONCURRENT_REQUESTS ? pending.length : await Promise.race(pending);

pending[idx] = markAllForPage(page, links).then( => idx); }	}

/*	 * Find out what page is associated with each revision, * and create a list of revisions for each page. */	async function getPageInfo(links) { const BATCH_SIZE = 50; let pages = new Map; let revids = [...links.keys];

for(let i = 0; i < revids.length; i += BATCH_SIZE) { let response = await api.get({				action : 'query',				prop : 'revisions|info',				rvprop : "ids|timestamp",				revids : revids.slice(i, i + BATCH_SIZE).join("|")			}).catch(handleApiError);

if (!response.query || !response.query.pages) continue; // All the revids were bad, perhaps?

for (let id in response.query.pages) { let page = pages.get(id); if (!page) pages.set(id, response.query.pages[id]); else { page.revisions.push(...response.query.pages[id].revisions); }			}		}

/*		 * Sort by timestamp (newest first), then by revid (largest first), * and remove duplicates */		for (let [id, page] of pages) { let r = page.revisions.slice;

r.sort((a, b) => {				if (a.timestamp == b.timestamp)					return a.revid == b.revid ? 0 : a.revid > b.revid ? -1 : 1;				else					return a.timestamp > b.timestamp ? -1 : 1;			});

page.revisions = [];

for(let i = 0; i < r.length; i++) if (i == 0 || r[i].revid != r[i - 1].revid) page.revisions.push(r[i]); }

return pages; }

/*	 * Extract revision ID from various forms of links, * (...diff=prev&oldid=xxx, Special:Diff/xxx, etc.) */	function parseLink(link) { let diff, oldid, match, p;

try { p = new mw.Uri(link); } catch(e) { return null; }

if (p.host !== mw.config.get('wgServerName')) return null;

if (p.path == mw.config.get('wgScript')) { diff = p.query.diff; oldid = p.query.oldid; } else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)$/i))) { diff = match[1]; } else if ((match = p.path.match(/^\/wiki\/Special:PermanentLink\/([^\/]+)$/i))) { oldid = match[1]; } else if ((match = p.path.match(/^\/wiki\/Special:Diff\/([^\/]+)\/([^\/]+)$/i))) { oldid = match[1]; diff = match[2]; }

switch(diff) { case undefined: case "prev": return parseInt(oldid) || null; case "cur": case "next": return null; // Not yet implemented default: return parseInt(diff) || null; }

return null; }

async function activate(event) { event.preventDefault;

if (api) api.abort; else { api = new mw.Api({               ajax: {                    headers: {                        'Api-User-Agent' : API_USER_AGENT                    }                }            });

mw.loader.load(CSS_PAGE, "text/css"); }

let links = new Map;

/*		 * Find any element with an associated revid, or any link * that is NOT descended from an element with a revid */		let $elems = $('#mw-content-text [data-mw-revid], #mw-content-text a:not([data-mw-revid] a)');

for (let e of $elems) { let $elem, revid = $(e).data('mwRevid') || parseLink(e.href);

// Not a permalink or diff if (!revid) continue;

// No data-mw-revid in ancestor  on AbuseLog if (mw.config.get('wgCanonicalSpecialPageName') == "AbuseLog") $elem = $(e).closest('li'); else $elem = $(e);

$elem.removeClass("external damaging mr-reverted mr-unknown mr-live mr-error");

if (!links.get(revid)) links.set(revid, $elem); else links.set(revid, links.get(revid).add($elem)); }

let pages = await getPageInfo(links);

if (pages.size > HARD_PAGE_LIMIT) { alert(mw.msg('mr-disallow-toomany', pages.size)); return; } else if (pages.size > SOFT_PAGE_LIMIT) { if (!confirm(mw.msg('mr-warn-toomany', pages.size))) return; }

markAll(pages, links); }

$.when(mw.loader.using( ["mediawiki.util", "mediawiki.api", "mediawiki.Uri"] ),		  $.ready).then( => {			   mw.messages.set(MESSAGES);

$(mw.util.addPortletLink( "p-tb", "#",				  mw.msg('mr-activate-text'), 't-markreverted', mw.msg('mr-activate-title') )).click(activate); }); }); //