Jump to content

User:Polygnotus/Scripts/XC.js

From Wikipedia, the free encyclopedia
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.
// 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);
});