User:Ahecht/sandbox/Scripts/watchlistcleaner.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.
//jshint maxerr:512
// Watchlist cleaner
function cleanWatchlist() {
	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
			};
			
			new mw.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 ([[User:Ahecht/Scripts/watchlistcleaner|Watchlist cleaner]])'
			};
			
			new mw.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"
			};
			
			new mw.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"
		};

		new mw.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 + ".";
		new mw.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 = '<a href="#" title="Run cleanwatchlist.js" id="clean-watchlist-link" rel data-event-name="tabs.">Clean the watchlist</a>';
		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 );
	}
} );