// Wikipedia User Block & Activity Checker for common.js
// Checks if users are blocked and if they've been active in the last 12 months
// Features: maxlag support, HTTP error handling, exponential backoff retry (1min, 3min, 5min), deduplication
async function checkUserBlocks() {
// Show input dialog immediately
showInputDialog();
}
// Helper function to scroll status area to bottom
function scrollStatusToBottom() {
setTimeout(() => {
const statusDiv = $('#status-text');
const statusArea = $('#status-area');
// Try scrolling the status area container instead of the text div
if (statusArea.length && statusArea[0]) {
statusArea[0].scrollTop = statusArea[0].scrollHeight;
}
// Also try the text div as backup
if (statusDiv.length && statusDiv[0]) {
statusDiv[0].scrollTop = statusDiv[0].scrollHeight;
}
}, 10);
}
function showInputDialog() {
const inputHtml = `
<div>
<p><strong>Enter usernames (one per line):</strong></p>
<p>Supported formats:</p>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>[[User:Username]]</li>
<li>[[User talk:Username]]</li>
<li>User:Username</li>
<li>User talk:Username</li>
</ul>
<textarea id="user-input" style="width: 100%; height: 200px; font-family: monospace;"
placeholder="Paste your usernames here..."></textarea>
<div id="status-area" style="margin-top: 10px; font-family: monospace; background: #f8f9fa; padding: 10px; border: 1px solid #ddd; height: 170px; overflow-y: auto; display: none;">
<div id="status-text"></div>
</div>
</div>
`;
const dialog = $('<div>').html(inputHtml).dialog({
title: 'User Block Checker',
width: 600,
height: 670,
modal: false,
resizable: true,
buttons: {
'Check Users': function() {
const input = $('#user-input').val().trim();
if (!input) {
alert('Please enter some usernames to check.');
return;
}
processUsers(input, dialog);
},
'Clear': function() {
$('#user-input').val('');
$('#status-area').hide();
$('#status-text').empty();
},
'Close': function() {
$(this).dialog('close');
}
}
});
// Focus the textarea
setTimeout(() => $('#user-input').focus(), 100);
}
// Helper function to check if username is a vanished/renamed user
function isVanishedOrRenamed(username) {
const lowerUsername = username.toLowerCase();
return lowerUsername.startsWith('vanished user') || lowerUsername.startsWith('renamed user');
}
async function processUsers(input, dialog) {
const allUsers = parseUsers(input);
const users = deduplicateUsers(allUsers);
if (users.length === 0) {
alert('No valid usernames found in the input.');
return;
}
// Show status area and start processing
$('#status-area').show();
const statusDiv = $('#status-text');
// Show deduplication info if there were duplicates
const duplicateCount = allUsers.length - users.length;
if (duplicateCount > 0) {
statusDiv.html(`<div>Found ${allUsers.length} usernames, removed ${duplicateCount} duplicates.</div><div>Checking ${users.length} unique users for blocks and activity (last 12 months)...</div>`);
console.log(`Found ${allUsers.length} usernames, removed ${duplicateCount} duplicates.`);
} else {
statusDiv.html(`<div>Checking ${users.length} users for blocks and activity (last 12 months)...</div>`);
}
scrollStatusToBottom();
console.log(`Checking ${users.length} users for blocks and activity...`);
const activeUsers = [];
const blockedUsers = [];
const inactiveUsers = [];
const vanishedUsers = []; // New category for vanished/renamed users
// Disable the Check Users button during processing
const checkButton = dialog.parent().find('.ui-dialog-buttonset button:contains("Check Users")');
checkButton.prop('disabled', true).text('Checking...');
for (let i = 0; i < users.length; i++) {
const userInfo = users[i];
const progress = `[${i + 1}/${users.length}]`;
statusDiv.append(`<div>${progress} Checking ${userInfo.username}...</div>`);
scrollStatusToBottom();
console.log(`${progress} Checking user: ${userInfo.username} ...`);
// Check if this is a vanished or renamed user first
if (isVanishedOrRenamed(userInfo.username)) {
vanishedUsers.push(userInfo.original);
statusDiv.append(`<div style="color: #999;">${progress} ◯ ${userInfo.username} is a vanished/renamed user (skipped)</div>`);
scrollStatusToBottom();
console.log(`◯ ${userInfo.username} is a vanished/renamed user (skipped)`);
// Add base delay between requests
if (i < users.length - 1) {
await sleep(500); // Shorter delay since we're not making API calls
}
continue;
}
try {
// Check if user is blocked first
const isBlocked = await isUserBlocked(userInfo.username);
if (isBlocked) {
blockedUsers.push(userInfo.original);
statusDiv.append(`<div style="color: #d33;">${progress} ✗ ${userInfo.username} is blocked</div>`);
scrollStatusToBottom();
console.log(`✗ ${userInfo.username} is blocked`);
} else {
// If not blocked, check activity
const isActive = await isUserActive(userInfo.username);
if (isActive) {
activeUsers.push(userInfo.original);
statusDiv.append(`<div style="color: #00af89;">${progress} ✓ ${userInfo.username} is active (not blocked + active in last 12 months)</div>`);
scrollStatusToBottom();
console.log(`✓ ${userInfo.username} is active (not blocked + active in last 12 months)`);
} else {
inactiveUsers.push(userInfo.original);
statusDiv.append(`<div style="color: #fc3;">${progress} ⚠ ${userInfo.username} is not blocked but inactive (no edits in last 12 months)</div>`);
scrollStatusToBottom();
console.log(`⚠ ${userInfo.username} is not blocked but inactive (no edits in last 12 months)`);
}
}
} catch (error) {
console.error(`Failed to check ${userInfo.username} after all retries:`, error);
activeUsers.push(userInfo.original);
statusDiv.append(`<div style="color: #fc3;">${progress} ? ${userInfo.username} - check failed, assuming active</div>`);
scrollStatusToBottom();
console.log(`? ${userInfo.username} - check failed, assuming active`);
}
// Add base delay between requests to avoid hammering the API
if (i < users.length - 1) { // Don't delay after the last user
await sleep(1000);
}
}
// Re-enable button and show completion
checkButton.prop('disabled', false).text('Check Users');
statusDiv.append(`<div style="font-weight: bold; margin-top: 10px;">✓ Completed! ${activeUsers.length} active, ${blockedUsers.length} blocked, ${inactiveUsers.length} inactive, ${vanishedUsers.length} vanished/renamed</div>`);
scrollStatusToBottom();
// Display results
console.log("\n=== RESULTS ===");
console.log(`\nActive users (not blocked + active in last 12 months) (${activeUsers.length}):`);
activeUsers.forEach(user => console.log(user));
console.log(`\nBlocked users (${blockedUsers.length}):`);
blockedUsers.forEach(user => console.log(user));
console.log(`\nInactive users (not blocked but no edits in last 12 months) (${inactiveUsers.length}):`);
inactiveUsers.forEach(user => console.log(user));
console.log(`\nVanished/Renamed users (${vanishedUsers.length}):`);
vanishedUsers.forEach(user => console.log(user));
// Show results in a separate dialog
displayResults(activeUsers, blockedUsers, inactiveUsers, vanishedUsers);
}
function parseUsers(input) {
const lines = input.split('\n');
const users = [];
// Regex patterns for different input formats
const patterns = [
/\[\[User:([^\]]+)\]\]/i, // [[User:Username]]
/\[\[User talk:([^\]]+)\]\]/i, // [[User talk:Username]]
/^User:(.+)$/i, // User:Username (full line)
/^User talk:(.+)$/i // User talk:Username (full line)
];
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;
for (const pattern of patterns) {
const match = trimmedLine.match(pattern);
if (match) {
users.push({
username: match[1].trim(),
original: trimmedLine
});
break;
}
}
}
return users;
}
function deduplicateUsers(users) {
const seen = new Set();
const uniqueUsers = [];
for (const user of users) {
// Use lowercase username for comparison to handle case variations
const normalizedUsername = user.username.toLowerCase();
if (!seen.has(normalizedUsername)) {
seen.add(normalizedUsername);
uniqueUsers.push(user);
}
}
return uniqueUsers;
}
async function isUserBlocked(username) {
const maxRetries = 3;
const retryDelays = [60000, 180000, 300000]; // 1min, 3min, 5min in milliseconds
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const api = new mw.Api();
const response = await api.get({
action: 'query',
format: 'json',
list: 'blocks',
bkusers: username,
bklimit: 1,
maxlag: 5 // Wait if server lag is more than 5 seconds
});
// If blocks array exists and has entries, user is blocked
return response.query.blocks && response.query.blocks.length > 0;
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed for ${username} (block check):`, error);
// Check if this is a maxlag error
if (error.code === 'maxlag') {
const lagTime = error.lag || 5;
console.log(`Server lag detected (${lagTime}s). Waiting before retry...`);
await sleep((lagTime + 1) * 1000); // Wait lag time + 1 second
continue;
}
// Check for HTTP error codes that warrant retry
if (isRetryableError(error)) {
if (attempt < maxRetries) {
const delay = retryDelays[attempt];
console.log(`Retryable error for ${username} (block check). Waiting ${delay / 1000}s before retry ${attempt + 2}...`);
await sleep(delay);
continue;
} else {
console.error(`Max retries exceeded for ${username} (block check). Final error:`, error);
throw new Error(`Failed after ${maxRetries + 1} attempts: ${error.message}`);
}
} else {
// Non-retryable error, fail immediately
console.error(`Non-retryable error for ${username} (block check):`, error);
throw error;
}
}
}
}
async function isUserActive(username) {
const maxRetries = 3;
const retryDelays = [60000, 180000, 300000]; // 1min, 3min, 5min in milliseconds
const cutoffDate = new Date();
cutoffDate.setMonth(cutoffDate.getMonth() - 12); // 12 months ago
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const api = new mw.Api();
const response = await api.get({
action: 'query',
format: 'json',
list: 'usercontribs',
ucuser: username,
uclimit: 1,
maxlag: 5 // Wait if server lag is more than 5 seconds
});
const usercontribs = response.query.usercontribs;
if (usercontribs && usercontribs.length > 0) {
const lastContrib = usercontribs[0];
const timestamp = lastContrib.timestamp;
// Parse timestamp (format: 2024-01-15T10:30:45Z)
const lastActivity = new Date(timestamp);
return lastActivity > cutoffDate;
}
return false; // No contributions found
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed for ${username} (activity check):`, error);
// Check if this is a maxlag error
if (error.code === 'maxlag') {
const lagTime = error.lag || 5;
console.log(`Server lag detected (${lagTime}s). Waiting before retry...`);
await sleep((lagTime + 1) * 1000); // Wait lag time + 1 second
continue;
}
// Check for HTTP error codes that warrant retry
if (isRetryableError(error)) {
if (attempt < maxRetries) {
const delay = retryDelays[attempt];
console.log(`Retryable error for ${username} (activity check). Waiting ${delay / 1000}s before retry ${attempt + 2}...`);
await sleep(delay);
continue;
} else {
console.error(`Max retries exceeded for ${username} (activity check). Final error:`, error);
throw new Error(`Failed after ${maxRetries + 1} attempts: ${error.message}`);
}
} else {
// Non-retryable error, fail immediately
console.error(`Non-retryable error for ${username} (activity check):`, error);
throw error;
}
}
}
}
function isRetryableError(error) {
// Check for HTTP status codes that warrant retry
if (error.xhr && error.xhr.status) {
const status = error.xhr.status;
// Retry on server errors (5xx) and some client errors
return status >= 500 || status === 429 || status === 408 || status === 502 || status === 503 || status === 504;
}
// Check for specific MediaWiki API error codes that warrant retry
if (error.code) {
const retryableCodes = [
'maxlag', // Server lag
'readonly', // Database in read-only mode
'internal_api_error_DBConnectionError',
'internal_api_error_DBQueryError',
'ratelimited' // Rate limiting
];
return retryableCodes.includes(error.code);
}
// Check for network-related errors
if (error.textStatus) {
const retryableStatus = ['timeout', 'error', 'abort'];
return retryableStatus.includes(error.textStatus);
}
return false;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function displayResults(activeUsers, blockedUsers, inactiveUsers, vanishedUsers) {
// Create a results dialog
const resultsHtml = `
<div style="max-height: 500px; overflow-y: auto;">
<h3>✓ Active Users (not blocked + active in last 12 months) (${activeUsers.length})</h3>
<textarea readonly style="width: 100%; height: 100px; font-family: monospace; margin-bottom: 10px;">
${activeUsers.join('\n')}</textarea>
<h3>✗ Blocked Users (${blockedUsers.length})</h3>
<textarea readonly style="width: 100%; height: 100px; font-family: monospace; margin-bottom: 10px;">
${blockedUsers.join('\n')}</textarea>
<h3>⚠ Inactive Users (not blocked but no edits in last 12 months) (${inactiveUsers.length})</h3>
<textarea readonly style="width: 100%; height: 100px; font-family: monospace; margin-bottom: 10px;">
${inactiveUsers.join('\n')}</textarea>
<h3>◯ Vanished/Renamed Users (${vanishedUsers.length})</h3>
<textarea readonly style="width: 100%; height: 100px; font-family: monospace;">
${vanishedUsers.join('\n')}</textarea>
</div>
`;
// Create and show results dialog (non-modal)
$('<div>').html(resultsHtml).dialog({
title: 'User Block & Activity Check Results',
width: 700,
height: 650,
modal: false,
resizable: true,
buttons: {
'Copy Active Users': function() {
navigator.clipboard.writeText(activeUsers.join('\n')).then(() => {
mw.notify('Active users copied to clipboard!', { type: 'success' });
}).catch(() => {
mw.notify('Failed to copy to clipboard', { type: 'error' });
});
},
'Copy Blocked Users': function() {
navigator.clipboard.writeText(blockedUsers.join('\n')).then(() => {
mw.notify('Blocked users copied to clipboard!', { type: 'success' });
}).catch(() => {
mw.notify('Failed to copy to clipboard', { type: 'error' });
});
},
'Copy Inactive Users': function() {
navigator.clipboard.writeText(inactiveUsers.join('\n')).then(() => {
mw.notify('Inactive users copied to clipboard!', { type: 'success' });
}).catch(() => {
mw.notify('Failed to copy to clipboard', { type: 'error' });
});
},
'Copy Vanished/Renamed Users': function() {
navigator.clipboard.writeText(vanishedUsers.join('\n')).then(() => {
mw.notify('Vanished/Renamed users copied to clipboard!', { type: 'success' });
}).catch(() => {
mw.notify('Failed to copy to clipboard', { type: 'error' });
});
},
'Close': function() {
$(this).dialog('close');
}
}
});
}
// Add a button to the page for easy access
function addBlockCheckerButton() {
if (mw.config.get('wgNamespaceNumber') === -1) return; // Don't add on special pages
const portletId = mw.config.get('skin') === 'vector' ? 'p-cactions' : 'p-tb';
mw.util.addPortletLink(
portletId,
'#',
'Check User Blocks & Activity',
't-check-blocks',
'Check if users are blocked and active (last 12 months)'
);
$('#t-check-blocks').on('click', function(e) {
e.preventDefault();
checkUserBlocks();
});
}
// Initialize when page loads
$(document).ready(function() {
addBlockCheckerButton();
});
// Also make the function available globally
window.checkUserBlocks = checkUserBlocks;