User:Polygnotus/Scripts/SortByEditcount.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 code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/SortByEditcount.
// <nowiki>
// Wikipedia User Edit Count Checker for common.js
// Gets edit counts for users with maxlag support, HTTP error handling, exponential backoff retry, deduplication
// Uses OOUI for modern interface
async function checkUserEditCounts() {
showEditCountDialog();
}
function showEditCountDialog() {
// Create input field for usernames
const inputField = new OO.ui.MultilineTextInputWidget({
placeholder: 'Enter usernames (one per line)\nSupported formats:\n[[User:Username]]\n[[User talk:Username]]\nUser:Username\nUser talk:Username',
rows: 10,
classes: ['editcount-input']
});
// Create output field for results
const outputField = new OO.ui.MultilineTextInputWidget({
placeholder: 'Edit counts will appear here...',
rows: 10,
readOnly: true,
classes: ['editcount-output']
});
// Create progress bar
const progressBar = new OO.ui.ProgressBarWidget({
progress: 0,
classes: ['editcount-progress']
});
progressBar.toggle(false); // Initially hidden
// Create status message
const statusMessage = new OO.ui.LabelWidget({
label: 'Ready to check edit counts',
classes: ['editcount-status']
});
// Create buttons
const checkButton = new OO.ui.ButtonWidget({
label: 'Check Edit Counts',
flags: ['primary', 'progressive'],
icon: 'search'
});
const clearButton = new OO.ui.ButtonWidget({
label: 'Clear',
icon: 'clear'
});
const copyButton = new OO.ui.ButtonWidget({
label: 'Copy Results',
icon: 'copy',
disabled: true
});
// Create simple layout without FieldLayout wrappers
const fieldsetLayout = new OO.ui.FieldsetLayout({
label: 'User Edit Count Checker',
items: [
new OO.ui.LabelWidget({ label: 'Input usernames:' }),
inputField,
new OO.ui.Widget({ content: [new OO.ui.LabelWidget({ label: 'Status:' })] }),
statusMessage,
new OO.ui.Widget({ content: [new OO.ui.LabelWidget({ label: 'Progress:' })] }),
progressBar,
new OO.ui.Widget({ content: [new OO.ui.LabelWidget({ label: 'Results:' })] }),
outputField,
new OO.ui.Widget({ content: [new OO.ui.LabelWidget({ label: 'Actions:' })] }),
new OO.ui.HorizontalLayout({
items: [checkButton, clearButton, copyButton]
})
]
});
// Create a ProcessDialog instead of Dialog to get proper close button
function EditCountDialog(config) {
EditCountDialog.parent.call(this, config);
}
OO.inheritClass(EditCountDialog, OO.ui.ProcessDialog);
EditCountDialog.static.name = 'editCountDialog';
EditCountDialog.static.title = 'User Edit Count Checker';
EditCountDialog.static.size = 'large';
// Define actions - primary action goes on right
EditCountDialog.static.actions = [
{
action: 'close',
label: 'Close',
flags: ['primary']
}
];
EditCountDialog.prototype.initialize = function() {
EditCountDialog.parent.prototype.initialize.call(this);
this.content = new OO.ui.PanelLayout({
padded: true,
expanded: false
});
this.content.$element.append(fieldsetLayout.$element);
this.$body.append(this.content.$element);
};
EditCountDialog.prototype.getSetupProcess = function(data) {
return EditCountDialog.parent.prototype.getSetupProcess.call(this, data)
.next(function() {
inputField.focus();
});
};
EditCountDialog.prototype.getActionProcess = function(action) {
return new OO.ui.Process(function() {
this.close({ action: action });
}, this);
};
EditCountDialog.prototype.getBodyHeight = function() {
return 800;
};
const dialog = new EditCountDialog({
classes: ['editcount-dialog']
});
// Button event handlers
checkButton.on('click', async function() {
const input = inputField.getValue().trim();
if (!input) {
mw.notify('Please enter some usernames to check.', { type: 'error' });
return;
}
await processUsersForEditCounts(input, checkButton, progressBar, statusMessage, outputField, copyButton);
});
clearButton.on('click', function() {
inputField.setValue('');
outputField.setValue('');
statusMessage.setLabel('Ready to check edit counts');
progressBar.setProgress(0);
progressBar.toggle(false);
copyButton.setDisabled(true);
});
copyButton.on('click', function() {
const results = outputField.getValue();
if (results) {
navigator.clipboard.writeText(results).then(() => {
mw.notify('Results copied to clipboard!', { type: 'success' });
}).catch(() => {
mw.notify('Failed to copy to clipboard', { type: 'error' });
});
}
});
// Create window manager and add dialog
const windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
windowManager.addWindows({ editCountDialog: dialog });
// Open dialog
windowManager.openWindow('editCountDialog');
}
async function processUsersForEditCounts(input, checkButton, progressBar, statusMessage, outputField, copyButton) {
const allUsers = parseUsers(input);
const users = deduplicateUsers(allUsers);
if (users.length === 0) {
mw.notify('No valid usernames found in the input.', { type: 'error' });
return;
}
// Show deduplication info
const duplicateCount = allUsers.length - users.length;
if (duplicateCount > 0) {
statusMessage.setLabel(`Found ${allUsers.length} usernames, removed ${duplicateCount} duplicates. Checking ${users.length} unique users...`);
console.log(`Found ${allUsers.length} usernames, removed ${duplicateCount} duplicates.`);
} else {
statusMessage.setLabel(`Checking ${users.length} users for edit counts...`);
}
// Show progress bar and disable check button
progressBar.setProgress(0);
progressBar.toggle(true);
checkButton.setDisabled(true);
checkButton.setLabel('Checking...');
const userResults = [];
const errors = [];
console.log(`Checking ${users.length} users for edit counts...`);
for (let i = 0; i < users.length; i++) {
const userInfo = users[i];
const progress = Math.round(((i + 1) / users.length) * 100);
progressBar.setProgress(progress);
statusMessage.setLabel(`[${i + 1}/${users.length}] Checking ${userInfo.username}...`);
console.log(`[${i + 1}/${users.length}] Checking edit count for: ${userInfo.username}`);
try {
const editCount = await getUserEditCount(userInfo.username);
userResults.push({
original: userInfo.original,
username: userInfo.username,
editCount: editCount
});
console.log(`✓ ${userInfo.username}: ${editCount.toLocaleString()} edits`);
} catch (error) {
console.error(`Failed to get edit count for ${userInfo.username}:`, error);
userResults.push({
original: userInfo.original,
username: userInfo.username,
editCount: 0,
error: true
});
errors.push(userInfo.username);
}
// Add delay between requests to avoid hammering the API
if (i < users.length - 1) {
await sleep(1000);
}
}
// Sort by edit count (highest first)
userResults.sort((a, b) => b.editCount - a.editCount);
// Update UI with completion
progressBar.setProgress(100);
statusMessage.setLabel(`✓ Completed! Checked ${users.length} users. ${errors.length > 0 ? `${errors.length} errors.` : 'No errors.'}`);
checkButton.setDisabled(false);
checkButton.setLabel('Check Edit Counts');
copyButton.setDisabled(false);
// Display results (just usernames, but ordered by edit count)
const sortedUsernames = userResults.map(result => result.original);
outputField.setValue(sortedUsernames.join('\n'));
console.log("\n=== EDIT COUNT RESULTS (sorted by edit count) ===");
userResults.forEach(result => {
if (result.error) {
console.log(`${result.original}: ERROR`);
} else {
console.log(`${result.original}: ${result.editCount.toLocaleString()} edits`);
}
});
if (errors.length > 0) {
console.log(`\nErrors occurred for: ${errors.join(', ')}`);
}
}
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;
let matched = false;
for (const pattern of patterns) {
const match = trimmedLine.match(pattern);
if (match) {
users.push({
username: match[1].trim(),
original: trimmedLine
});
matched = true;
break;
}
}
// If no pattern matched, treat as plain username
if (!matched) {
users.push({
username: trimmedLine,
original: trimmedLine
});
}
}
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 getUserEditCount(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: 'users',
ususers: username,
usprop: 'editcount',
maxlag: 5 // Wait if server lag is more than 5 seconds
});
if (response.query.users && response.query.users.length > 0) {
const user = response.query.users[0];
// Check if user exists and has edit count
if (user.missing) {
return 0; // User doesn't exist
}
return user.editcount || 0;
}
return 0; // No user data returned
} catch (error) {
console.warn(`Attempt ${attempt + 1} failed for ${username} (edit count):`, 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} (edit count). Waiting ${delay / 1000}s before retry ${attempt + 2}...`);
await sleep(delay);
continue;
} else {
console.error(`Max retries exceeded for ${username} (edit count). 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} (edit count):`, 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));
}
// Add a button to the page for easy access
function addEditCountButton() {
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 Edit Counts',
't-check-editcounts',
'Get edit counts for multiple users'
);
$('#t-check-editcounts').on('click', function(e) {
e.preventDefault();
checkUserEditCounts();
});
}
// Add some basic CSS for better appearance
mw.loader.using('oojs-ui-core').then(function() {
mw.util.addCSS(`
.editcount-dialog .oo-ui-window-body {
font-family: sans-serif;
height: 800px !important;
}
.editcount-dialog .oo-ui-window-frame {
height: 800px !important;
}
.editcount-input textarea,
.editcount-output textarea {
font-family: monospace !important;
}
.editcount-progress {
margin: 10px 0;
}
.editcount-status {
font-weight: bold;
color: #0645ad;
}
`);
});
// Initialize when page loads
$(document).ready(function() {
// Wait for OOUI to be available
mw.loader.using('oojs-ui-core').then(function() {
addEditCountButton();
});
});
// Also make the function available globally
window.checkUserEditCounts = checkUserEditCounts;
// </nowiki>