Twitch Auto Channel Points Claimer Redux

Automatically claim channel points with minimal performance impact.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name Twitch Auto Channel Points Claimer Redux
// @version 1.0.0
// @author Jeffenson
// @description Automatically claim channel points with minimal performance impact.
// @match https://www.twitch.tv/*
// @match https://dashboard.twitch.tv/*
// @license MIT
// @grant none
// @namespace https://greasyfork.org/users/983748
// ==/UserScript==

(function() {
    // Configuration options
    const config = {
        enableLogging: true,
        enableDebug: false,        
        minDelay: 2000,
        maxAdditionalDelay: 1000,
        primaryCheckInterval: 3000,   // Main interval for checking points (ms)
        fastCheckDuration: 10000,     // Duration to use fast checking after page load (ms)
        fastCheckInterval: 1000,      // Fast check interval during initial period (ms)
        observerMode: 'minimal',      // 'none', 'minimal', or 'full'
        observerThrottleTime: 2000,   // Minimum time between observer-triggered checks
        continuousOperation: true     // Keep script running during navigation transitions
    };

    // State variables
    let claiming = false;
    let observer = null;
    let checkInterval = null;
    let fastCheckInterval = null;
    let fastCheckTimeout = null;
    let urlCheckInterval = null;
    let statusInterval = null;
    let lastCheckTime = 0;
    let startTime = new Date();
    let instanceId = Math.random().toString(36).substring(2, 10);

    // Track original history methods
    let originalPushState = null;
    let originalReplaceState = null;

    // Debug statistics
    const stats = {
        intervalChecks: 0,
        observerChecks: 0,
        manualChecks: 0,
        bonusFound: 0,
        claimAttempts: 0,
        successfulClaims: 0,
        errors: 0,
        navigationEvents: 0,
        reinitializations: 0
    };

    // Logging functions
    function log(message) {
        if (config.enableLogging) {
            console.log(`[Channel Points Claimer ${instanceId}] ${message}`);
        }
    }

    function debug(message) {
        if (config.enableDebug) {
            console.debug(`[Channel Points Debug ${instanceId}] ${message}`);
        }
    }

    function logStats() {
        if (config.enableDebug) {
            const runTime = Math.round((new Date() - startTime) / 1000);
            console.group(`Channel Points Claimer ${instanceId} - Debug Statistics`);
            console.log(`Runtime: ${runTime} seconds`);
            console.log(`Interval checks: ${stats.intervalChecks}`);
            console.log(`Observer-triggered checks: ${stats.observerChecks}`);
            console.log(`Manual checks: ${stats.manualChecks}`);
            console.log(`Bonus elements found: ${stats.bonusFound}`);
            console.log(`Claim attempts: ${stats.claimAttempts}`);
            console.log(`Successful claims: ${stats.successfulClaims}`);
            console.log(`Errors: ${stats.errors}`);
            console.log(`Navigation events: ${stats.navigationEvents}`);
            console.log(`Reinitializations: ${stats.reinitializations}`);
            console.log(`Current page: ${window.location.href}`);
            console.log(`Observer mode: ${config.observerMode}`);
            console.log(`Active intervals: ${checkInterval ? 'Main✓' : 'Main✗'} ${fastCheckInterval ? 'Fast✓' : 'Fast✗'} ${urlCheckInterval ? 'URL✓' : 'URL✗'}`);
            console.groupEnd();
        }
    }

    // Check for and claim bonus
    function checkForBonus(source = 'interval') {
        // Track check source
        if (source === 'interval') stats.intervalChecks++;
        else if (source === 'observer') stats.observerChecks++;
        else if (source === 'manual') stats.manualChecks++;

        // Throttle checks
        const now = Date.now();
        if (now - lastCheckTime < 500) { // Minimum 500ms between any checks
            return false;
        }
        lastCheckTime = now;

        try {
            // More specific selector targeting
            const bonusSelectors = [
                '.claimable-bonus__icon',
                '[data-test-selector="community-points-claim"]',
                '.community-points-summary button[aria-label*="Claim"]',
                '.channel-points-reward-button',
                'button[aria-label="Claim Bonus"]',
                'button[data-a-target="chat-claim-bonus-button"]',
                // Add more selectors if Twitch changes their UI
            ];

            let bonus = null;
            for (const selector of bonusSelectors) {
                const elements = document.querySelectorAll(selector);
                if (elements && elements.length > 0) {
                    // Try to find the most visible/interactive element
                    for (const element of elements) {
                        if (element.offsetParent !== null && !element.disabled && element.style.display !== 'none') {
                            bonus = element;
                            debug(`Found bonus with selector: ${selector} (source: ${source})`);
                            break;
                        }
                    }
                    if (bonus) break;
                }
            }

            if (bonus) {
                stats.bonusFound++;

                if (!claiming) {
                    stats.claimAttempts++;
                    debug(`Attempting to claim bonus (attempt #${stats.claimAttempts})`);

                    try {
                        bonus.click();
                        const date = new Date().toLocaleTimeString();
                        claiming = true;

                        // Random delay before allowing another claim
                        const claimDelay = config.minDelay + (Math.random() * config.maxAdditionalDelay);

                        setTimeout(() => {
                            stats.successfulClaims++;
                            log(`Claimed at ${date} (total: ${stats.successfulClaims})`);
                            claiming = false;

                            // After claiming, do a quick follow-up check in case there are multiple bonuses
                            setTimeout(() => checkForBonus('follow-up'), 500);
                        }, claimDelay);

                        return true;
                    } catch (clickError) {
                        stats.errors++;
                        log(`Error clicking bonus: ${clickError.message}`);
                        claiming = false;
                        return false;
                    }
                } else {
                    debug('Bonus found but still in claiming cooldown');
                }
            }
            return false;
        } catch (error) {
            stats.errors++;
            log(`Error in checkForBonus: ${error.message}`);
            debug(`Stack trace: ${error.stack}`);
            return false;
        }
    }

    // Set up the primary interval-based checking system
    function setupIntervalChecker() {
        debug('Setting up interval checkers');

        // Clear any existing intervals
        if (checkInterval) {
            clearInterval(checkInterval);
            checkInterval = null;
            debug('Cleared existing main interval');
        }

        if (fastCheckInterval) {
            clearInterval(fastCheckInterval);
            fastCheckInterval = null;
            debug('Cleared existing fast interval');
        }

        if (fastCheckTimeout) {
            clearTimeout(fastCheckTimeout);
            fastCheckTimeout = null;
            debug('Cleared existing fast timeout');
        }

        // Set up the main checking interval
        checkInterval = setInterval(() => {
            checkForBonus('interval');
        }, config.primaryCheckInterval);

        debug(`Main interval checker set up with ${config.primaryCheckInterval}ms interval`);

        // Initially use a faster check interval for a short period after page load
        fastCheckInterval = setInterval(() => {
            checkForBonus('fast-interval');
        }, config.fastCheckInterval);

        debug(`Fast checking enabled with ${config.fastCheckInterval}ms interval`);

        fastCheckTimeout = setTimeout(() => {
            if (fastCheckInterval) {
                clearInterval(fastCheckInterval);
                fastCheckInterval = null;
                debug('Fast checking period ended, cleared interval');
            }
        }, config.fastCheckDuration);

        debug(`Fast checking will end after ${config.fastCheckDuration}ms`);
    }

    // Set up a minimal observer that only triggers on very specific changes
    function setupMinimalObserver() {
        if (config.observerMode === 'none') {
            debug('Observer disabled by configuration');
            return;
        }

        const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
        if (!MutationObserver) {
            log('MutationObserver not supported in this browser');
            return;
        }

        // Clean up any existing observer
        if (observer) {
            observer.disconnect();
            observer = null;
            debug('Cleared existing points observer');
        }

        observer = new MutationObserver(mutations => {
            // Only process if we see specific bonus-related changes
            const relevantChange = mutations.some(mutation => {
                // Check if this is a relevant element
                if (mutation.target && mutation.target.className &&
                    /claimable|claim-button|bonus|points-reward/i.test(mutation.target.className)) {
                    return true;
                }

                // Check added nodes for bonus elements
                if (mutation.addedNodes && mutation.addedNodes.length) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 && node.className &&
                            /claimable|claim-button|bonus|points-reward/i.test(node.className)) {
                            return true;
                        }
                    }
                }

                return false;
            });

            if (relevantChange) {
                debug('Detected relevant DOM change for channel points');
                checkForBonus('observer');
            }
        });

        // Find the most specific target possible
        const pointsContainerSelectors = [
            '.community-points-summary',
            '.channel-points-container',
            '.chat-input__buttons-container',
            '.chat-input',
            '.chat-room'
        ];

        let targetNode = null;
        for (const selector of pointsContainerSelectors) {
            const element = document.querySelector(selector);
            if (element) {
                targetNode = element;
                debug(`Found specific observer target: ${selector}`);
                break;
            }
        }

        if (!targetNode) {
            if (config.observerMode === 'minimal') {
                debug('No specific target found for minimal observer, skipping observer setup');
                return;
            }
            targetNode = document.querySelector('.right-column') || document.body;
            debug(`Using fallback observer target: ${targetNode.tagName}`);
        }

        // Very selective observation configuration
        const observerConfig = {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'data-test-selector', 'aria-label'],
            characterData: false
        };

        observer.observe(targetNode, observerConfig);
        debug(`Observer set up in ${config.observerMode} mode on ${targetNode.tagName}`);
    }

    // Restore original history methods
    function restoreHistoryMethods() {
        if (originalPushState) {
            history.pushState = originalPushState;
            debug('Restored original pushState');
        }

        if (originalReplaceState) {
            history.replaceState = originalReplaceState;
            debug('Restored original replaceState');
        }
    }

    // Complete cleanup function for page navigation
    function cleanup() {
        debug('Running cleanup');

        if (observer) {
            observer.disconnect();
            observer = null;
            debug('Observer disconnected');
        }

        if (checkInterval) {
            clearInterval(checkInterval);
            checkInterval = null;
            debug('Main interval checker stopped');
        }

        if (fastCheckInterval) {
            clearInterval(fastCheckInterval);
            fastCheckInterval = null;
            debug('Fast interval checker stopped');
        }

        if (fastCheckTimeout) {
            clearTimeout(fastCheckTimeout);
            fastCheckTimeout = null;
            debug('Fast check timeout cleared');
        }

        if (!config.continuousOperation) {
            // Only clear URL check interval if not in continuous mode
            if (urlCheckInterval) {
                clearInterval(urlCheckInterval);
                urlCheckInterval = null;
                debug('URL check interval stopped');
            }

            // Only clear status interval if not in continuous mode
            if (statusInterval) {
                clearInterval(statusInterval);
                statusInterval = null;
                debug('Status interval stopped');
            }

            // Restore original history methods
            restoreHistoryMethods();

            // Remove popstate listener
            window.removeEventListener('popstate', checkForUrlChange);
        }

        logStats();
    }

    // Function to check URL changes
    function checkForUrlChange() {
        const currentUrl = location.href;
        if (currentUrl !== window.lastTwitchUrl) {
            stats.navigationEvents++;
            debug(`URL changed from ${window.lastTwitchUrl} to ${currentUrl} (navigation #${stats.navigationEvents})`);
            window.lastTwitchUrl = currentUrl;

            if (config.continuousOperation) {
                // In continuous mode, we keep the URL checker running
                // but reinitialize the points checkers
                if (checkInterval) {
                    clearInterval(checkInterval);
                    checkInterval = null;
                }

                if (fastCheckInterval) {
                    clearInterval(fastCheckInterval);
                    fastCheckInterval = null;
                }

                if (fastCheckTimeout) {
                    clearTimeout(fastCheckTimeout);
                    fastCheckTimeout = null;
                }

                if (observer) {
                    observer.disconnect();
                    observer = null;
                }

                // Do a final check before reinitializing
                checkForBonus('navigation');

                // Reinitialize with delay
                debug('Waiting 1.5 seconds before re-initializing checkers');
                setTimeout(() => {
                    debug('Re-initializing checkers after navigation');
                    stats.reinitializations++;
                    setupIntervalChecker();
                    setupMinimalObserver();
                }, 1500);
            } else {
                // Complete cleanup in non-continuous mode
                cleanup();

                // Reinitialize with delay
                debug('Waiting 1.5 seconds before re-initializing');
                setTimeout(() => {
                    debug('Re-initializing after navigation');
                    stats.reinitializations++;
                    initialize();
                }, 1500);
            }
        }
    }

    // Handle page navigation
    function setupPageListeners() {
        debug('Setting up page navigation listeners');

        // Clean up when leaving the page
        window.addEventListener('beforeunload', () => {
            debug('Page unloading, cleaning up');
            cleanup();
        });

        // Store initial URL in a global variable to avoid closure issues
        window.lastTwitchUrl = location.href;
        debug(`Initial URL: ${window.lastTwitchUrl}`);

        // Clear any existing URL check interval
        if (urlCheckInterval) {
            clearInterval(urlCheckInterval);
            urlCheckInterval = null;
            debug('Cleared existing URL check interval');
        }

        // Set up a dedicated interval for URL checking
        urlCheckInterval = setInterval(checkForUrlChange, 1000);
        debug('URL check interval set up');

        // Store original history methods
        if (!originalPushState) {
            originalPushState = history.pushState;
        }
        if (!originalReplaceState) {
            originalReplaceState = history.replaceState;
        }

        // Override history methods
        history.pushState = function() {
            originalPushState.apply(this, arguments);
            debug('History pushState detected');
            checkForUrlChange();
        };

        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            debug('History replaceState detected');
            checkForUrlChange();
        };

        // And listen for popstate events
        window.removeEventListener('popstate', checkForUrlChange); // Remove any existing listener
        window.addEventListener('popstate', checkForUrlChange);
    }

    // Periodic status reporting
    function setupStatusReporting() {
        const REPORT_INTERVAL = 60000; // 1 minute

        if (statusInterval) {
            clearInterval(statusInterval);
            statusInterval = null;
            debug('Cleared existing status interval');
        }

        statusInterval = setInterval(() => {
            debug('Periodic status check:');

            // Check if we're on a channel page
            const onChannelPage = /twitch\.tv\/(?!directory|settings|u|p|user|videos|subscriptions|inventory|wallet)/.test(window.location.href);
            debug(`Current URL: ${window.location.href} (on channel page: ${onChannelPage})`);

            // Check for channel points elements
            const pointsContainer = document.querySelector('.community-points-summary, .channel-points-container');
            debug(`Points container present: ${!!pointsContainer}`);

            // Check if intervals are still running
            debug(`Active intervals: ${checkInterval ? 'Main✓' : 'Main✗'} ${fastCheckInterval ? 'Fast✓' : 'Fast✗'} ${urlCheckInterval ? 'URL✓' : 'URL✗'}`);

            // Log full stats
            logStats();

            // Do a manual check just to be safe
            checkForBonus('periodic');
        }, REPORT_INTERVAL);

        debug('Status reporting set up');
    }

    // Initialize everything
    function initialize() {
        log('Script starting');
        debug(`Version 2.1.0 - Final Release Version`);
        debug(`Instance ID: ${instanceId}`);
        debug(`User agent: ${navigator.userAgent}`);
        debug(`Current URL: ${window.location.href}`);

        startTime = new Date();
        lastCheckTime = 0;

        // First do an immediate check
        setTimeout(() => {
            debug('Running initial check');
            checkForBonus('initial');
        }, 1000);

        // Set up the primary interval-based system
        setupIntervalChecker();

        // Set up the minimal observer if enabled
        setupMinimalObserver();

        // Set up page navigation listeners
        setupPageListeners();

        // Set up status reporting
        setupStatusReporting();
    }

    // Start the script
    initialize();

    // Expose debug controls to console
    window.channelPointsDebug = {
        config: config,
        stats: stats,
        logStats: logStats,
        checkNow: () => {
            const result = checkForBonus('manual');
            return result ? "Bonus found and claimed!" : "No bonus available at this time";
        },
        reinitialize: () => {
            cleanup();
            initialize();
            return "Script reinitialized";
        },
        toggleDebug: () => {
            config.enableDebug = !config.enableDebug;
            log(`Debug mode ${config.enableDebug ? 'enabled' : 'disabled'}`);
            return config.enableDebug;
        },
        setObserverMode: (mode) => {
            if (['none', 'minimal', 'full'].includes(mode)) {
                config.observerMode = mode;
                cleanup();
                initialize();
                return `Observer mode set to ${mode}`;
            }
            return `Invalid mode. Use 'none', 'minimal', or 'full'`;
        },
        toggleContinuousOperation: () => {
            config.continuousOperation = !config.continuousOperation;
            log(`Continuous operation ${config.continuousOperation ? 'enabled' : 'disabled'}`);
            return config.continuousOperation;
        },
        instanceId: instanceId,
        cleanup: () => {
            cleanup();
            return "Manual cleanup completed";
        }
    };

    debug('Debug controls available via window.channelPointsDebug');
})();