- // ==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://gf.qytechs.cn/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');
- })();