// 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 proper dialog class
function EditCountDialog(config) {
EditCountDialog.parent.call(this, config);
}
OO.inheritClass(EditCountDialog, OO.ui.Dialog);
EditCountDialog.static.name = 'editCountDialog';
EditCountDialog.static.title = 'User Edit Count Checker';
EditCountDialog.static.size = 'large';
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) {
if (action === 'close') {
return new OO.ui.Process(function() {
this.close({ action: action });
}, this);
}
return EditCountDialog.parent.prototype.getActionProcess.call(this, action);
};
EditCountDialog.static.actions = [
{
action: 'close',
label: 'Close',
flags: ['safe', 'close']
}
];
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);
try {
const editCount = await getUserEditCount(userInfo.username);
results.push(userInfo.original);{
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);
results.push(userInfo.original);{
original: userInfo.original,
username: userInfo.username,
editCount: -1 // Use -1 to indicate error, will sort to bottom
});
errors.push(userInfo.username);
}
}
}
// Sort results by edit count (descending order)
results.sort((a, b) => {
// Put errors at the bottom
if (a.editCount === -1 && b.editCount !== -1) return 1;
if (b.editCount === -1 && a.editCount !== -1) return -1;
if (a.editCount === -1 && b.editCount === -1) return 0;
// Sort by edit count (highest first)
return b.editCount - a.editCount;
});
// Update UI with completion
copyButton.setDisabled(false);
// Display results with edit counts
outputField.setValue(const formattedResults = results.joinmap('\n'));result => {
if (result.editCount === -1) {
return `${result.original} - ERROR`;
}
return `${result.original} - ${result.editCount.toLocaleString()} edits`;
});
outputField.setValue(formattedResults.join('\n'));
console.log("\n=== EDIT COUNT RESULTS (sorted by edit count) ===");
resultsformattedResults.forEach(result => console.log(result));
if (errors.length > 0) {
}
}
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: 680px !important;
}
.editcount-dialog .oo-ui-window-frame {
height: 680px !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;
|