User:DannyS712/FindIPActivity.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
// Quick script to find where an IP has been active recently
// @author DannyS712
$(() => {
const FindIPActivity = {};
window.FindIPActivity = FindIPActivity;

FindIPActivity.init = function () {
	window.document.title = 'FindIPActivity';
	$( '#firstHeading' ).text( 'FindIPActivity' );
	mw.loader.using(
		[ 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'mediawiki.api', 'mediawiki.ForeignApi', 'mediawiki.util' ],
		FindIPActivity.run
	);
};

FindIPActivity.run = function () {
	var preloadIP = mw.util.getParamValue( 'targetIP' );
	var targetIP = new OO.ui.TextInputWidget( {
		value: preloadIP
	} );
	var targetIPLayout = new OO.ui.FieldLayout(
		targetIP,
		{ label: 'Target IP' }
	);
	
	var submit = new OO.ui.ButtonInputWidget( { 
		label: 'Search',
		flags: [
			'primary',
			'progressive'
		]
	} );
	var clickHandler = function () {
		console.log( targetIP );
		FindIPActivity.onSubmit( targetIP.value );
	};
	submit.on( 'click', clickHandler );
	$( window ).on( 'keypress', function ( e ) {
		// press enter to start
		if ( e.which == 13 ) {
			submit.simulateLabelClick();
			clickHandler();
		}
	} );
	
	var fieldSet = new OO.ui.FieldsetLayout( { 
		label: 'Show a list of wikis where an IP has edited in the last 24 hours (the edits might be deleted though)'
	} );
	
	fieldSet.addItems( [
		targetIPLayout,
		new OO.ui.FieldLayout( submit )
	] );

	var $results = $( '<div>' )
		.attr( 'id', 'FindIPActivity-results' );
	$( '#mw-content-text' ).empty().append(
		$( '<span>' ).append(
			'Source code: ',
			$( '<a>' )
				.attr( 'href', 'https://meta.wikimedia.org/wiki/User:DannyS712/FindIPActivity.js' )
				.attr( 'target', '_blank' )
				.text( '[[:m:User:DannyS712/FindIPActivity.js]]' )
		),
		fieldSet.$element,
		$( '<hr>' ),
		$results
	);
	
	// Trigger automatically from preload
	if ( preloadIP ) {
		submit.simulateLabelClick();
		clickHandler();
	}
};

FindIPActivity.onSubmit = function ( targetIP ) {
	var $res = $( '#FindIPActivity-results' );
	$res.empty();
	$res.append( $( '<p>' ).text( 'Getting a list of wikis to check...' ) );
	console.log( targetIP );
	FindIPActivity.getSitesList().then(
		function ( allSiteURLs ) {
			console.log( 'Checking against:' );
			console.log( allSiteURLs );
			$res.append( $( '<p>' ).text( 'Checking the APIs...' ) );
			FindIPActivity.getSitesWithEdits( allSiteURLs, targetIP ).then(
				function ( allSitesWithEdits ) {
					console.log( 'Found recent edits on:' );
					console.log( allSitesWithEdits );
					FindIPActivity.showResults( allSitesWithEdits, targetIP );
				}
			);
		},
		FindIPActivity.onErrHandler
	);
};

FindIPActivity.onErrHandler = function () {
	// Shared error handler
	alert( 'Something went wrong' );
	console.log( arguments );
};

// So we only need to query the SiteMatrix API once
FindIPActivity.siteListCache = false;
FindIPActivity.getSitesList = function () {
	return new Promise( function ( resolve ) {
		if ( FindIPActivity.siteListCache !== false ) {
			resolve( FindIPActivity.siteListCache );
			return;
		}
		new mw.Api().get( {
			action: 'sitematrix',
			smsiteprop: 'url',
			smlimit: 'max',
			formatversion: 2
		} ).then(
			function ( response ) {
				console.log( response );
				// Future proofing - at some point there max be more than 500 language
				// codes, and I'll need to add continuation logic. Until then
				if ( response[ 'query-continue' ] ) {
					alert( 'The script currently does not have continuation logic, only some sites may be shown. Contact DannyS712' );
				}
				
				var allSiteURLs = [];
				// response.sitematrix has integer keys for the language entries,
				// a `count`, and an array of special wikis. We handle specials
				// later
				Object.keys( response.sitematrix ).forEach( function ( key ) {
					if ( key === 'count' || key === 'specials' ) {
						return;
					}
					response.sitematrix[ key ].site.forEach( function ( langSite ) {
						// 'closed' is reserved, can't use dot notation
						if ( !( langSite['closed'] ) ) {
							allSiteURLs.push( langSite.url );
						}
					} );
				} );
				response.sitematrix.specials.forEach( function( specialWiki ) {
					// 'closed' is reserved, can't use dot notation
					if ( !(
						specialWiki.private ||
						specialWiki['closed'] ||
						specialWiki.fishbowl ||
						specialWiki.nonglobal
					) ) {
						allSiteURLs.push( specialWiki.url );
					}
				} );
				allSiteURLs = allSiteURLs.map(
					function ( url ) {
						return url.replace( /^https?:/, '' );
					}
				);
				console.log( allSiteURLs );
				FindIPActivity.siteListCache = allSiteURLs;
				resolve( allSiteURLs );
			},
			FindIPActivity.onErrHandler
		);
	} );
};

// So that it can be set in the checkSite method
FindIPActivity.sitesWithEdits = [];
FindIPActivity.getSitesWithEdits = function ( allSiteURLs, targetIP ) {
	// clear out sitesWithEdits, then map each url to a promise that resolves
	// once the site is checked; within that check, sitesWithEdits is updated
	// if the specific site has edits
	FindIPActivity.sitesWithEdits = [];
	return new Promise( function ( resolve ) {
		Promise.all(
			allSiteURLs.map(
				function ( siteURL ) {
					return FindIPActivity.checkSite( siteURL, targetIP );
				}
			)
		).then(
			function () {
				console.log( 'has edits on:' );
				console.log( FindIPActivity.sitesWithEdits );
				resolve( FindIPActivity.sitesWithEdits );
			}
		);
	} );
};
FindIPActivity.checkSite = function ( url, targetIP ) {
	return new Promise( function ( resolve ) {
		// Checking if there is an edit made by the IP within the last 24 hours
		// Get the time from 24 hours ago, in UNIX timestamp format (seconds since January 1, 1970)
		// Date.now returns milliseconds
		var startTS = Math.floor( ( Date.now() / 1000 ) - ( 60 * 60 * 24 ) );
		new mw.ForeignApi( url + '/w/api.php' ).get( {
			action: 'query',
			list: 'allrevisions|alldeletedrevisions',
			
			// allrevisions props
			arvlimit: 1, // just checking if there is one
			arvuser: targetIP,
			arvdir: 'newer',
			arvstart: startTS, // newer than 24 hours ago
			
			// alldeletedrevisions props
			adrlimit: 1,
			adruser: targetIP,
			adrdir: 'newer',
			adrstart: startTS,
			adrprop: 'ids',
			
			formatversion: 2
		} ).then(
			function ( response ) {
				console.log( response );
				// could have existing or deleted
				var haveEdits = (
					response.query.allrevisions.length !== 0 ||
					response.query.alldeletedrevisions.length !== 0
				);
				if ( haveEdits ) {
					FindIPActivity.sitesWithEdits.push( url );
				}
				resolve( haveEdits );
			},
			function () {
				FindIPActivity.onErrHandler( arguments );
				// assume false
				resolve( false );
			}
			
		);
	} );
};

FindIPActivity.showResults = function ( allSitesWithEdits, targetIP ) {
	var $res = $( '#FindIPActivity-results' );
	if ( allSitesWithEdits.length === 0 ) {
		$res.append(
			$( '<p>' ).text( 'No edits found in the last 24 hours' )
		);
		return;
	}
	$res.append(
		$( '<p>' ).text( 'Recent edits found:' )
	);
	var $contribsLinks = $( '<ul>' );
	allSitesWithEdits.forEach(
		function ( siteWithEdits ) {
			$contribsLinks.append(
				$( '<li>' ).append(
					$( '<a>' )
						.attr( 'href', siteWithEdits + '/wiki/Special:Contributions/' + targetIP )
						.attr( 'target', '_blank' )
						.text( siteWithEdits.replace( /^\/\//, '' ) + '/wiki/Special:Contributions/' + targetIP )
				)
			);
		}
	);
	$res.append( $contribsLinks );
};

});

$( document ).ready( () => {
	mw.loader.using(
		[ 'mediawiki.util' ],
		function () {
			// Always add a sidebar link
			mw.util.addPortletLink(
				'p-tb',
				'/wiki/Special:BlankPage/FindIPActivity',
				'FindIPActivity'
			);
			
			// On IP contributions, add to p-views with preloaded
			if ( mw.config.get( 'wgCanonicalSpecialPageName' ) == 'Contributions' ) {
				var user = mw.config.get( 'wgRelevantUserName' );
				if ( user && mw.util.isIPAddress( user ) ) {
					mw.util.addPortletLink(
						'p-views',
						'/wiki/Special:BlankPage/FindIPActivity?targetIP=' + user,
						'FindIPActivity'
					);
				}
			}
		}
	);
	if ( mw.config.get( 'wgNamespaceNumber' ) === -1 ) {
		const page = mw.config.get( 'wgCanonicalSpecialPageName' );
		if ( page === 'Blankpage' ) {
			const page2 = mw.config.get( 'wgTitle' ).split( '/' );
			if ( page2[1] && page2[1] === 'FindIPActivity' ) {
				window.FindIPActivity.init();
			}
		}
	}
});

// </nowiki>