User:Polygnotus/Scripts/SortByEditcount.js

This is an old revision of this page, as edited by Polygnotus (talk | contribs) at 13:45, 8 July 2025 (Created page with '// 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)\nSuppor...'). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// 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 layout
    const fieldsetLayout = new OO.ui.FieldsetLayout({
        label: 'User Edit Count Checker',
        items: [
            new OO.ui.FieldLayout(inputField, {
                label: 'Input usernames:',
                align: 'top'
            }),
            new OO.ui.FieldLayout(statusMessage, {
                label: 'Status:',
                align: 'top'
            }),
            new OO.ui.FieldLayout(progressBar, {
                label: 'Progress:',
                align: 'top'
            }),
            new OO.ui.FieldLayout(outputField, {
                label: 'Results:',
                align: 'top'
            }),
            new OO.ui.FieldLayout(
                new OO.ui.HorizontalLayout({
                    items: [checkButton, clearButton, copyButton]
                }),
                {
                    label: 'Actions:',
                    align: 'top'
                }
            )
        ]
    });

    // Create dialog
    const dialog = new OO.ui.Dialog({
        size: 'large',
        classes: ['editcount-dialog']
    });

    dialog.initialize = function() {
        OO.ui.Dialog.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);
    };

    dialog.getSetupProcess = function(data) {
        return OO.ui.Dialog.prototype.getSetupProcess.call(this, data)
            .next(function() {
                inputField.focus();
            });
    };

    // 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([dialog]);

    // Open dialog
    windowManager.openWindow(dialog);
}

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 results = [];
    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);
            results.push(`${userInfo.original}: ${editCount.toLocaleString()} edits`);
            console.log(`✓ ${userInfo.username}: ${editCount.toLocaleString()} edits`);
        } catch (error) {
            console.error(`Failed to get edit count for ${userInfo.username}:`, error);
            results.push(`${userInfo.original}: ERROR - ${error.message}`);
            errors.push(userInfo.username);
        }

        // Add delay between requests to avoid hammering the API
        if (i < users.length - 1) {
            await sleep(1000);
        }
    }

    // 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
    outputField.setValue(results.join('\n'));

    console.log("\n=== EDIT COUNT RESULTS ===");
    results.forEach(result => console.log(result));
    
    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;
        }
        .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;