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

});

}

});

});

});

//