User:Ahecht/Scripts/watchlistcleaner.js

//jshint maxerr:512

// Watchlist cleaner

function cleanWatchlist() {

var api = new mw.Api( { userAgent: 'WatchlistCleaner/0.0.1' } );

var millisDay = 24*60*60*1000;

var cleanMiss = confirm("Remove redlinked (missing) pages from Watchlist?\n\n(OK for yes, Cancel for no)");

if (cleanMiss) {

var keepMissTalk = confirm("Skip removing redlinked pages if talk page exists?\n\n(OK for yes, Cancel for no)");

}

var cleanRedir = confirm("Remove redirects from Watchlist?\n\n(OK for yes, Cancel for no)");

var cleanOld = confirm("Remove pages from Watchlist you haven't recently edited (slow)?\n\n(OK for yes, Cancel for no)");

if (cleanOld) {

cleanOld = prompt("Minimum number of days since your last edit:");

cleanOld = Number(cleanOld) ?

new Date(new Date() - (Number(cleanOld)*millisDay)) :

false;

}

var cleanNever = confirm("Remove pages from Watchlist you have never edited (slow)?\n\n(OK for yes, Cancel for no)");

var keepCreations = confirm("Skip removing pages you created (slow)?\n\n(OK for yes, Cancel for no)");

var potentialUnwatch = [], unwatchPages = [], unwatchPagesCount = 0;

var potentiallyStale = [], potentiallyStaleCount = 0, potentiallyStalePercent = -1;

var statusText = "Fetching watchlist...";

function doUnwatch() { // Recursively unwatch pages in batches of 50

if (unwatchPages.length > 0) { // Still have pages to unwatch

console.log("Pages to unwatch: ");

console.log(unwatchPages);

statusText = "Removing " + unwatchPages.length + " pages from watchlist...";

mw.notify(statusText, {type: 'info', tag: 'status', autoHide: false});

var uwTitles = unwatchPages.splice(0,50).join("|"); // Remove 50 items from top of list

var params = {

action: "watch",

unwatch: "true",

titles: uwTitles

};

api.postWithToken("watch", params ).done( function(reslt) {

console.log("Unwatch successful: ");

console.log(reslt);

doUnwatch();

} ).fail( function(code, reslt) {

console.error("API error when unwatching pages: ");

console.error(reslt);

statusText = "API error when unwatching pages: " + code;

mw.notify(statusText, {type: 'error', tag: 'error'});

return;

} );

} else { // No more pages to unwatch

statusText = "Done. Removed " + unwatchPagesCount + " pages from watchlist";

mw.notify(statusText, {type: 'success', tag: 'status', autoHide: true});

}

}

function doWlBackup() {

unwatchPagesCount = unwatchPages.length;

var foundText = "Found " + unwatchPagesCount + " pages to remove.";

if (unwatchPagesCount == 0) {

mw.notify(foundText, {type: 'success', tag: 'found'});

return;

} else if ( !confirm("Remove " + unwatchPagesCount + " pages from watchlist?") ) {

mw.notify("Watchlist cleaner cancelled.", {type: 'error', tag: 'status', autoHide: true});

return;

} else if ( confirm("Backup removed pages?\n\n(OK for yes, Cancel for no)") ) {

var wlBackupLocation = mw.config.get('wgFormattedNamespaces')[2]

+ ":" + mw.config.get('wgUserName') + "/Watchlist_backup";

var params = {

action: 'edit',

title: wlBackupLocation,

section: 'new',

sectiontitle: new Date().toISOString(),

text: '* :' + unwatchPages.join("\n* :") + '',

summary: 'Backup pages removed from watchlist (Watchlist cleaner)'

};

api.postWithToken("csrf", params ).done( function(reslt) {

console.log(wlBackupLocation + " updated:");

console.log(reslt);

statusText = unwatchPagesCount + " pages saved to "

+ wlBackupLocation + ".";

mw.notify(statusText, {type: 'success', tag: 'status', autoHide: true});

doUnwatch();

} ).fail( function(code, error) {

console.error("API error when saving backup: ");

console.error(error);

statusText = "API error when saving backup: " + code;

mw.notify(statusText, {type: 'error', tag: 'error'});

return;

} );

} else {

mw.notify(foundText, {type: 'warn', tag: 'found'});

doUnwatch();

}

}

function removeCreations(potentialUnwatchCount = 0, potentialUnwatchPercent = -1) {

if (!keepCreations) { // Don't filter page creations

unwatchPages = potentialUnwatch;

doWlBackup();

} else if (potentialUnwatch.length == 0) { // Done filtering

doWlBackup();

} else { // Filter page creations

if(!potentialUnwatchCount) {

potentialUnwatchCount = potentialUnwatch.length;

}

var tempPUPercent = 100 - Math.ceil(100 * potentialUnwatch.length / potentialUnwatchCount);

if (tempPUPercent != potentialUnwatchPercent) {

potentialUnwatchPercent = tempPUPercent;

var statusText = "Checking for your pages you created... ("+ tempPUPercent + "%)";

mw.notify(statusText, {type: 'info', tag: 'status', autoHide: false});

}

var query = {

prop: 'revisions',

titles: potentialUnwatch.shift(),

rvprop: 'user',

rvlimit: '1',

rvdir: 'newer',

formatversion: "2"

};

api.get( query )

.done (function (d) {

if(d && d.query && d.query.pages && d.query.pages[0] &&

d.query.pages[0].revisions && d.query.pages[0].revisions[0]) { // Page found

d=d.query.pages[0].revisions[0];

if(d.user && d.user == mw.config.get('wgUserName')) {

console.log("Keeping page " + query.titles + ", which you created.");

foundText = "Keeping page " + query.titles + ", which you created.";

mw.notify(foundText, {type: 'warn', tag: 'found'});

} else {

unwatchPages.push(query.titles);

}

} else {

unwatchPages.push(query.titles);

}

removeCreations(potentialUnwatchCount, potentialUnwatchPercent);

} ).fail (function(code, error) {

console.error("API error when fetching page creator: ");

console.error(error);

statusText = "API error fetching page creator: " + code;

mw.notify(statusText, {type: 'error', tag: 'error'});

unwatchPages.push(query.titles);

removeCreations(potentialUnwatchCount, potentialUnwatchPercent);

} );

}

}

function isPageStale(checkPage, checkAssoc, pageStatus = {exists: false, newEdit: false, everEdit: false}) {

var query = {

prop: 'revisions',

titles: checkPage,

rvprop: 'timestamp',

rvlimit: '1',

rvuser: mw.config.get('wgUserName'),

formatversion: "2"

};

api.get( query )

.done (function (d) {

if (d && d.query && d.query.pages && d.query.pages[0]) { //API query returned pages

pageStatus.exists = true;

if (d.query.pages[0].revisions && d.query.pages[0].revisions[0].timestamp) { //User edit found

pageStatus.everEdit = true;

if (cleanOld) {

var revDate = new Date(d.query.pages[0].revisions[0].timestamp);

if ( revDate > cleanOld ) { // New revision found

if (!checkAssoc) {

console.log ("User edit on " + checkPage + " is new enough.");

}

pageStatus.newEdit = true;

} else { // Last revision exists but is too old

console.log ("Old user edit found on " + checkPage + " from " + revDate);

}

} else if ( (pageStatus.everEdit === false) && !checkAssoc ) {

console.log("User edit found on " + checkPage);

}

} else { // No user edits found

console.log ("No user edits found on " + checkPage);

}

} // No page returned by API

if ( (cleanOld && pageStatus.newEdit === false) ||

(cleanNever && pageStatus.everEdit === false) ) {

if (checkAssoc) { // Talk page exists to check

console.log("Checking talk page...");

isPageStale(checkAssoc, false, pageStatus);

} else { //already on talk page

checkStalePages(pageStatus);

}

} else { // Page passed

checkStalePages(pageStatus);

}

} ).fail (function(code, error) {

console.error("API error when fetching revisions: ");

console.error(error);

statusText = "API error fetching revisions: " + code;

mw.notify(statusText, {type: 'error', tag: 'error'});

removeCreations();

} );

}

function checkStalePages(pageStatus) {

var tempPSPercent = 100 - Math.ceil(100 * potentiallyStale.length / potentiallyStaleCount);

if (tempPSPercent != potentiallyStalePercent) {

potentiallyStalePercent = tempPSPercent;

var statusText = "Checking for your last edit... ("+ tempPSPercent + "%)";

mw.notify(statusText, {type: 'info', tag: 'status', autoHide: false});

}

var currentPage = potentiallyStale.shift();

if(currentPage) {

if(pageStatus.exists) { // Page exists

if (cleanNever && pageStatus.everEdit === false) { // No user edits found

foundText = "" + currentPage[0] + " has not been edited by you ever.";

mw.notify(foundText, {type: 'warn', tag: 'found'});

potentialUnwatch.push(currentPage[0]);

} else if (cleanOld && pageStatus.everEdit === true && pageStatus.newEdit === false) { // No new edits found

foundText = "" + currentPage[0] + " has not been edited by you recently.";

mw.notify(foundText, {type: 'warn', tag: 'found'});

potentialUnwatch.push(currentPage[0]);

} // Page is okay

} // Page doesn't exist

if (potentiallyStale[0]) {

isPageStale(potentiallyStale[0][0], potentiallyStale[0][1]);

} else { // No more pages in list

console.log("Finished checking for old and unedited pages");

removeCreations();

}

} else { // No more pages in list

console.log("Finished checking for old and unedited pages");

removeCreations();

}

}

function fetchWatchlist(cont) { // Recursively fetch watchlist

var query = {

action: "query",

prop: "info",

inprop: "associatedpage|talkid",

generator: "watchlistraw",

gwrlimit: "max",

formatversion: "2"

};

if (cont) {

query = Object.assign(query, cont);

}

mw.notify(statusText, {type: 'info', tag: 'status', autoHide: false});

statusText = statusText + ".";

api.get( query )

.done (function (d) {

if (d && d.query && d.query.pages) { //API query returned pages

d.query.pages.forEach( function(i) {

if(i.ns % 2 == 0) { // Page isn't a talk page

if(cleanMiss && i.missing){ // Add missing page to list

mw.notify("Found missing page " + i.title + ".", {type: 'warn', tag: 'found'});

if (keepMissTalk && !i.talkid) {

mw.notify("Talk page of " + i.title + " exists, skipping.", {type: 'warn', tag: 'found'});

} else {

potentialUnwatch.push(i.title);

}

} else if (cleanRedir && i.redirect) { // Add redirect to list

mw.notify("Found redirect " + i.title + ".", {type: 'warn', tag: 'found'});

potentialUnwatch.push(i.title);

} else if (cleanOld || cleanNever) { // Add pages to check revisions

potentiallyStale.push([i.title, i.associatedpage]);

}

}

} );

}

if (d && d.continue) { // More results are available

fetchWatchlist(d.continue);

} else if (potentiallyStale[0] && (cleanOld || cleanNever)) {

// No more results, check stale and missing

potentiallyStaleCount = potentiallyStale.length;

isPageStale(potentiallyStale[0][0], potentiallyStale[0][1]);

} else { // No more results, no potentially stale pages or not checking

removeCreations();

}

} ).fail (function(code, error) {

console.error("API error when fetching watchlist: ");

console.error(error);

statusText = "API error fetching watchlist: " + code;

mw.notify(statusText, {type: 'error', tag: 'error'});

} );

return;

}

if (cleanMiss || cleanRedir || cleanOld || cleanNever) { // Cancel wasn't selected for all options

fetchWatchlist();

}

}

$(document).ready( function() { // Add "Clean" link to toolbar

if( /Watchlist$/.test(mw.config.get('wgCanonicalSpecialPageName')) ) {

var cleanLink = 'Clean the watchlist';

if ($('.mw-watchlist-toollinks').length > 0) { //Most older skins

$('.mw-watchlist-toollinks a').last().after(' | ' + cleanLink);

} else if ($("#p-associated-pages").length > 0) { //Vector-2022 or Minerva

var lastLi = $("#p-associated-pages li").last();

lastLi.clone().

attr("id", (lastLi.attr("id") || "").replace(/(\d+)$/, function(){return arguments[1]*1+1;}))

.html(cleanLink).insertAfter(lastLi);

} else { //Fallback to "Tools" menu

mw.util.addPortletLink( 'p-tb', '#', 'Clean the watchlist', 'clean-watchlist-link', 'Run cleanwatchlist.js');

}

$("#clean-watchlist-link").on("click", cleanWatchlist );

}

} );