User:GeneralNotability/mark-locked.js

// // @ts-check // Companion to markblocked - asynchronously marks locked users // Chunks borrowed from User:Krinkle/Scripts/CVNSimpleOverlay_wiki.js, // User:GeneralNotability/ip-ext-info.js, and MediaWiki:Gadget-markblocked.js

/** * Get all userlinks on the page * * @param {JQuery} $content page contents * @return {Map} list of unique users on the page and their corresponding links */ function lockedUsers_getUsers($content) { const userLinks = new Map;

// Get all aliases for user: & user_talk: (taken from markblocked) const userNS = []; for (const ns in mw.config.get( 'wgNamespaceIds' ) ) { if (mw.config.get('wgNamespaceIds')[ns] === 2 || mw.config.get('wgNamespaceIds')[ns] === 3) { userNS.push(mw.util.escapeRegExp(ns.replace(/_/g, ' ')) + ':'); }	}

// RegExp for all titles that are User:| User_talk: | Special:Contributions/ (for userscripts) const userTitleRX = new RegExp('^(' + userNS.join('|') + '|Special:Contrib(?:ution)?s\\/|Special:CentralAuth\\/)+([^\\/#]+)$', 'i'); const articleRX = new RegExp(mw.config.get('wgArticlePath').replace('$1', '') + '([^#]+)'); const redlinkRX = new RegExp(mw.config.get('wgScript') + '\\?title=([^#&]+)'); $('a', $content).each(function {		if (!$(this).attr('href')) {			// Ignore if the  doesn't have a href			return;		}		let articleTitleReMatch = articleRX.exec($(this).attr('href').toString);		if (!articleTitleReMatch) {			// Try the redlink check			articleTitleReMatch = redlinkRX.exec($(this).attr('href').toString);			if (!articleTitleReMatch) {				return;			}		}		let pgTitle;		try {			pgTitle = decodeURIComponent(articleTitleReMatch[1]).replace(/_/g, ' ');		} catch (error) {			// Happens sometimes on non-username paths, like if there's a slash in the path			return;		}		const userTitleReMatch = userTitleRX.exec(pgTitle);		if (!userTitleReMatch) {			return;		}		const username = userTitleReMatch[2];		if (!mw.util.isIPAddress(username, true)) {			if (!userLinks.get(username)) {				userLinks.set(username, []);			}			userLinks.get(username).push($(this));		}	}); return userLinks; }

/** * Check whether a user is locked and if they are, return details about it * * @param {string} user Username to check * * @return {Promise } Whether the user in question is locked and a formatted tooltip about the lock */ async function lockedUsers_getLock(user) { let locked = false; let tooltip = ''; // Ensure consistent case conversions with PHP as per https://phabricator.wikimedia.org/T292824 user = new mw.Title(user).getMain; const api = new mw.ForeignApi('https://meta.wikimedia.org/w/api.php'); // Pre-check whether they're locked at all - if no, return early try { const response = await api.get({			action: 'query',			list: 'globalallusers',			agulimit: '1',			agufrom: user,			aguto: user,			aguprop: 'lockinfo'		}); if (response.query.globalallusers.length === 0) { // If the length is 0, then we couldn't find the global user return { locked, tooltip }; }		// If the 'locked' field is present, then the user is locked if (!('locked' in response.query.globalallusers[0])) { return { locked, tooltip }; }	} catch (error) { return { locked, tooltip }; }

try { const response = await api.get({			action: 'query',			list: 'logevents',			leprop: 'user|timestamp|comment|details',			leaction: 'globalauth/setstatus',			letitle: `User:${user}@global`		});

// If the length is 0, then we couldn't find the log event for (let logEvent of response.query.logevents) { let isLockEvent = false; // only works for more recent log entries, but most accurate try { isLockEvent = logEvent.params.added.includes('locked'); } catch (error) {} // Older style log entries if (!isLockEvent) { try { isLockEvent = logEvent.params[0] == 'locked'; } catch (error) {} }

if (isLockEvent) { const timestamp = new Date(logEvent.timestamp); const prettyTimestamp = lockedUsers_formatTimeSince(timestamp); tooltip = `Locked by ${logEvent.user}: ${logEvent.comment} (${prettyTimestamp} ago)`; locked = true; // Intentionally not breaking - cycle through to find the most recent lock in case there are multiple }		}	} catch (error) {}

return { locked, tooltip }; }

/** * Formats time since a date. Taken from mark-blocked.js * * @param {targetDate} Date to check the time since for * * @return {string} A prettified string regarding time since the lock occured */ function lockedUsers_formatTimeSince(targetDate) { const lockedUsers_padNumber = (number) => number <= 9 ? '0' + number : number;

const msSince = new Date - targetDate;

let minutes = Math.floor(msSince / 60000); if (!minutes) { return Math.floor(msSince / 1000) + 's'; }

let hours = Math.floor(minutes / 60); minutes %= 60;

let days = Math.floor(hours / 24); hours %= 24; if (days) { return `${days}${(days < 10 ? '.' + lockedUsers_padNumber(hours) : '' )}d`; }	return `${hours}:${lockedUsers_padNumber(minutes)}`; }

// On window load, get all the users on the page and check if they're blocked $.when( $.ready, mw.loader.using( 'mediawiki.util' ) ).then( function {	mw.hook('wikipage.content').add(function ($content) { const usersOnPage = lockedUsers_getUsers($content); usersOnPage.forEach(async (val, key, _) => {			const { locked, tooltip } = await lockedUsers_getLock(key);			if (locked) {				val.forEach(($link) => { $link.css({ opacity: 0.4, 'border-bottom-size': 'thick', 'border-bottom-style': 'dashed', 'border-bottom-color': 'red' }); $link.attr('title', tooltip); });			}		});	}); }); //