From 0d6ab38b488ddddbd0426014ca9ec51cce317f8a Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Wed, 5 Nov 2025 22:44:16 +0100 Subject: [PATCH] Improved interactive mode: added an automatic job reset option for scheduled tasks and improved stealth mode to bypass CAPTCHAs. --- src/browser/Browser.ts | 89 ++++++++++++++++++++++++++++++++++++++--- src/config.jsonc | 3 +- src/index.ts | 24 +++++++++-- src/interface/Config.ts | 1 + 4 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index 01597b1..bb2ffd7 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -29,20 +29,23 @@ class Browser { const envForceHeadless = process.env.FORCE_HEADLESS === '1' let headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false) - if (this.bot.isBuyModeEnabled() && !envForceHeadless) { + // Buy/Interactive mode: always visible and with enhanced stealth + const isBuyMode = this.bot.isBuyModeEnabled() + if (isBuyMode && !envForceHeadless) { if (headless !== false) { const target = this.bot.getBuyModeTarget() - this.bot.log(this.bot.isMobile, 'BROWSER', `Buy mode: forcing headless=false${target ? ` for ${target}` : ''}`, 'warn') + this.bot.log(this.bot.isMobile, 'BROWSER', `Interactive mode: forcing headless=false${target ? ` for ${target}` : ''}`, 'warn') } headless = false } const engineName = 'chromium' - this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) + this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless}${isBuyMode ? ', stealth-mode=ENHANCED' : ''})`) const proxyConfig = this.buildPlaywrightProxy(proxy) const isLinux = process.platform === 'linux' + // Base arguments for stability const baseArgs = [ '--no-sandbox', '--mute-audio', @@ -52,7 +55,7 @@ class Browser { '--ignore-ssl-errors' ] - // Linux stability fixes without detection risk + // Linux stability fixes const linuxStabilityArgs = isLinux ? [ '--disable-dev-shm-usage', '--disable-software-rasterizer', @@ -60,10 +63,26 @@ class Browser { '--disk-cache-size=1' ] : [] + // ENHANCED STEALTH MODE for Buy/Interactive Mode + // These arguments help bypass CAPTCHA and automation detection + const stealthArgs = isBuyMode ? [ + '--disable-blink-features=AutomationControlled', // Critical: Hide automation + '--disable-features=IsolateOrigins,site-per-process', // Reduce detection surface + '--disable-site-isolation-trials', + '--disable-web-security', // Allow cross-origin (may help with CAPTCHA) + '--disable-features=VizDisplayCompositor', // Reduce GPU fingerprinting + '--no-first-run', + '--no-default-browser-check', + '--disable-infobars', + '--window-position=0,0', + '--window-size=1920,1080', // Consistent window size + '--start-maximized' + ] : [] + browser = await playwright.chromium.launch({ headless, ...(proxyConfig && { proxy: proxyConfig }), - args: [...baseArgs, ...linuxStabilityArgs], + args: [...baseArgs, ...linuxStabilityArgs, ...stealthArgs], timeout: isLinux ? 90000 : 60000 }) } catch (e: unknown) { @@ -87,6 +106,8 @@ class Browser { const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000 context.setDefaultTimeout(typeof globalTimeout === 'number' ? globalTimeout : this.bot.utils.stringToMs(globalTimeout)) + const isBuyMode = this.bot.isBuyModeEnabled() + try { context.on('page', async (page) => { try { @@ -96,6 +117,7 @@ class Browser { await page.setViewportSize(viewport) + // Standard styling await page.addInitScript(() => { try { const style = document.createElement('style') @@ -109,6 +131,63 @@ class Browser { document.documentElement.appendChild(style) } catch {/* ignore */} }) + + // ENHANCED ANTI-DETECTION for Buy/Interactive Mode + if (isBuyMode) { + await page.addInitScript(` + // Override navigator.webdriver (critical for CAPTCHA bypass) + Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { + get: () => false + }); + + // Add chrome runtime (looks more human) + Object.defineProperty(window, 'chrome', { + writable: true, + enumerable: true, + configurable: false, + value: { runtime: {} } + }); + + // Add plugins (looks more human) + Object.defineProperty(navigator, 'plugins', { + get: () => [ + { name: 'Chrome PDF Plugin' }, + { name: 'Chrome PDF Viewer' }, + { name: 'Native Client' } + ] + }); + + // Languages + Object.defineProperty(navigator, 'languages', { + get: () => ['en-US', 'en', 'fr'] + }); + + // Hide automation markers + ['__nightmare', '__playwright', '__pw_manual', '__webdriver_script_fn', 'webdriver'].forEach(prop => { + try { + if (prop in window) delete window[prop]; + } catch {} + }); + + // Override permissions to avoid detection + const originalPermissionsQuery = window.navigator.permissions.query; + window.navigator.permissions.query = function(params) { + if (params.name === 'notifications') { + return Promise.resolve({ + state: Notification.permission, + name: 'notifications', + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => true + }); + } + return originalPermissionsQuery.call(this, params); + }; + `) + + this.bot.log(this.bot.isMobile, 'BROWSER', '🛡️ Enhanced stealth mode activated (anti-CAPTCHA)', 'log', 'green') + } } catch (e) { this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') } diff --git a/src/config.jsonc b/src/config.jsonc index 5e2570f..f7882e9 100644 --- a/src/config.jsonc +++ b/src/config.jsonc @@ -25,7 +25,8 @@ }, "jobState": { "enabled": true, - "dir": "" + "dir": "", + "autoResetOnComplete": true // Set to true to automatically rerun all accounts without prompting (for scheduled tasks) }, // Tasks diff --git a/src/index.ts b/src/index.ts index 36bad6f..5c7bb7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -171,9 +171,27 @@ export class MicrosoftRewardsBot { } private async promptResetJobState(): Promise { - // Skip prompt in non-interactive environments (Docker, CI, scheduled tasks) - if (!process.stdin.isTTY) { - log('main','TASK','Non-interactive environment detected - keeping job state', 'warn') + // Check if auto-reset is enabled in config (for scheduled tasks) + if (this.config.jobState?.autoResetOnComplete === true) { + log('main','TASK','Auto-reset enabled (jobState.autoResetOnComplete=true) - resetting and rerunning all accounts', 'log', 'green') + return true + } + + // Check environment variable override + const envAutoReset = process.env.REWARDS_AUTO_RESET_JOBSTATE + if (envAutoReset === '1' || envAutoReset?.toLowerCase() === 'true') { + log('main','TASK','Auto-reset enabled (REWARDS_AUTO_RESET_JOBSTATE) - resetting and rerunning all accounts', 'log', 'green') + return true + } + + // Detect non-interactive environments more reliably + const isNonInteractive = !process.stdin.isTTY || + process.env.CI === 'true' || + process.env.DOCKER === 'true' || + process.env.SCHEDULED_TASK === 'true' + + if (isNonInteractive) { + log('main','TASK','Non-interactive environment detected - keeping job state (set jobState.autoResetOnComplete=true to auto-rerun)', 'warn') return false } diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 374cc94..1740162 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -156,6 +156,7 @@ export interface ConfigJobState { enabled?: boolean; // default true dir?: string; // base directory; defaults to /job-state skipCompletedAccounts?: boolean; // if true (default), skip accounts already completed for the day + autoResetOnComplete?: boolean; // if true, automatically reset and rerun without prompting (useful for scheduled tasks) } // Live logging configuration