Greasy Fork is available in English.
Automatically claim channel points with minimal performance impact.
// ==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 r###lt = checkForBonus('manual'); return r###lt ? "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'); })();