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 = null, oldid = null, match, p;

try {

p = new URL(link);

} catch(e) {

return null;

}

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

return null;

if (p.pathname == mw.config.get('wgScript')) {

diff = p.searchParams.get("diff");

oldid = p.searchParams.get("oldid");

console.log(link, p, p.searchParams, diff, oldid);

} else if ((match = p.pathname.match(/^\/wiki\/Special:Diff\/([^\/]+)$/i))) {

diff = match[1];

} else if ((match = p.pathname.match(/^\/wiki\/Special:PermanentLink\/([^\/]+)$/i))) {

oldid = match[1];

} else if ((match = p.pathname.match(/^\/wiki\/Special:Diff\/([^\/]+)\/([^\/]+)$/i))) {

oldid = match[1];

diff = match[2];

}

switch(diff) {

case null:

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"] ),

    $.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);

    });

    })();

    //