User:Polygnotus/Scripts/XC.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:Polygnotus/Scripts/XC. |
// ExtendedConfirmedChecker.js
// Adds indicators next to usernames on talk pages showing extended confirmed status
// License: copyleft
$(function() {
'use strict';
// Only run on talk pages
const namespace = mw.config.get('wgNamespaceNumber');
console.log('Current namespace:', namespace);
if (namespace % 2 !== 1) {
console.log('Not a talk page, exiting');
return;
}
console.log('Running on talk page');
// Cache handling
const CACHE_KEY = 'ec-status-cache';
const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
function loadCache() {
try {
const cache = localStorage.getItem(CACHE_KEY);
if (cache) {
const { data, timestamp } = JSON.parse(cache);
if (Date.now() - timestamp < CACHE_EXPIRY) {
return new Map(Object.entries(data));
}
}
} catch (e) {
console.error('Error loading cache:', e);
}
return new Map();
}
function saveCache(cache) {
try {
const cacheData = {
data: Object.fromEntries(cache),
timestamp: Date.now()
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
} catch (e) {
console.error('Error saving cache:', e);
}
}
// Define advanced groups that imply extended confirmed status
const ADVANCED_GROUPS = new Set([
'sysop', // Administrators
'bot', // Bots
'checkuser', // CheckUsers
'oversight', // Oversighters
'founder', // Founders
'steward', // Stewards
'staff', // Wikimedia staff
'bureaucrat', // Bureaucrats
'extendedconfirmed' // Explicitly extended confirmed
]);
const processedUsers = new Set();
const userGroups = loadCache();
// Check if a URL path is a subpage
function isSubpage(path) {
// First decode the URL to handle any encoded characters
const decodedPath = decodeURIComponent(path);
// Remove any URL parameters or fragments
const cleanPath = decodedPath.split(/[?#]/)[0];
// Check if there's a slash after "User:"
return /User:[^/]+\//.test(cleanPath);
}
// Find all user links in signatures
function findUserLinks() {
// Find both regular user links and redlinks, excluding talk pages and user subpages
const links = $('#content a').filter(function() {
const href = $(this).attr('href');
// Basic check for user page links
if (!href || (!href.startsWith('/wiki/User:') && !href.startsWith('/w/index.php?title=User:'))) {
return false;
}
// Exclude talk pages
if (href.includes('talk')) {
return false;
}
// Exclude already processed links
if ($(this).attr('data-ec-checked')) {
return false;
}
// Check for subpages
if (href.startsWith('/wiki/')) {
if (isSubpage(href)) {
return false;
}
} else {
// For redlinks, check the title parameter
const url = new URL(href, window.location.origin);
const title = url.searchParams.get('title');
if (title && isSubpage(title)) {
return false;
}
}
return true;
});
console.log('Found user links:', links.length);
links.each((_, link) => {
const username = getUsernameFromLink(link);
console.log('User link:', $(link).text(), '→', username, $(link).attr('href'));
});
return links;
}
// Extract username from link
function getUsernameFromLink(link) {
const href = $(link).attr('href');
let match;
// Handle both regular wiki links and redlinks
if (href.startsWith('/wiki/')) {
match = decodeURIComponent(href).match(/User:([^/?&#]+)/);
} else {
// For redlinks, check the title parameter
const url = new URL(href, window.location.origin);
const title = url.searchParams.get('title');
if (title) {
match = decodeURIComponent(title).match(/User:([^/?&#]+)/);
}
}
if (match) {
// Remove any subpage part if it somehow got through
const username = match[1].split('/')[0];
return username.replace(/_/g, ' ');
}
return null;
}
// Check if user has any advanced group
function hasAdvancedGroup(groups) {
return groups.some(group => ADVANCED_GROUPS.has(group));
}
// Batch process users to reduce API calls
async function processUserBatch(users) {
if (users.length === 0) return;
const userList = users.join('|');
console.log('Fetching groups for users:', userList);
const maxRetries = 3;
let retryCount = 0;
let delay = 1000; // Start with 1 second delay
while (retryCount < maxRetries) {
try {
const response = await $.ajax({
url: mw.util.wikiScript('api'),
data: {
action: 'query',
format: 'json',
list: 'users',
usprop: 'groups|blockinfo',
ususers: userList,
formatversion: '2'
},
dataType: 'json'
});
console.log('API response:', response);
if (response.error && response.error.code === 'ratelimited') {
console.log('Rate limited, waiting before retry...');
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
retryCount++;
continue;
}
if (response.query && response.query.users) {
response.query.users.forEach(user => {
let status;
if (user.missing) {
status = 'missing';
} else if (user.blockedby) {
status = 'blocked';
} else {
const groups = user.groups || [];
// Check if user has any advanced group
status = hasAdvancedGroup(groups) ? 'extended' : 'normal';
}
userGroups.set(user.name, status);
});
// Save updated cache
saveCache(userGroups);
}
break; // Success, exit retry loop
} catch (error) {
console.error('Error fetching user groups:', error);
if (retryCount >= maxRetries - 1) {
// Mark all users in batch as error if we've exhausted retries
users.forEach(username => userGroups.set(username, 'error'));
saveCache(userGroups);
} else {
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
retryCount++;
}
}
}
}
// Add status indicator next to username
function addStatusIndicator(link, status) {
// Remove any existing indicators next to this link
$(link).siblings('.ec-status-indicator').remove();
let symbol, color, title;
switch(status) {
case 'extended':
symbol = '✔';
color = '#00a000';
title = 'Extended confirmed user';
break;
case 'error':
symbol = '?';
color = '#666666';
title = 'Error checking status';
break;
case 'blocked':
symbol = '🚫';
color = '#cc0000';
title = 'Blocked user';
break;
case 'missing':
symbol = '!';
color = '#666666';
title = 'User not found';
break;
default:
symbol = '✘';
color = '#cc0000';
title = 'Not extended confirmed';
}
const indicator = $('<span>')
.addClass('ec-status-indicator')
.css({
'margin-left': '4px',
'font-size': '0.85em',
'color': color,
'cursor': 'help'
})
.attr('title', title)
.text(symbol);
$(link).after(indicator);
$(link).attr('data-ec-checked', 'true');
}
// Main processing function
async function processPage() {
console.log('Processing page...');
const userLinks = findUserLinks();
const batchSize = 50;
const users = [];
userLinks.each((_, link) => {
const username = getUsernameFromLink(link);
if (username && !processedUsers.has(username)) {
users.push(username);
processedUsers.add(username);
}
});
// Process users in batches
for (let i = 0; i < users.length; i += batchSize) {
const batch = users.slice(i, i + batchSize);
await processUserBatch(batch);
}
// Add indicators
userLinks.each((_, link) => {
const username = getUsernameFromLink(link);
const isExtendedConfirmed = userGroups.get(username);
addStatusIndicator(link, isExtendedConfirmed);
});
}
// Run on page load and when new content is added
processPage();
mw.hook('wikipage.content').add(processPage);
});