From cbd05d128e177d75a75921619a2463c6d74058f9 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Fri, 14 Nov 2025 20:56:48 +0100 Subject: [PATCH] fix: Improve error handling and logging across multiple modules; enhance compatibility for legacy formats --- src/account-creation/AccountCreator.ts | 11 ++--- src/browser/Browser.ts | 2 +- src/browser/BrowserFunc.ts | 2 +- src/browser/BrowserUtil.ts | 13 +++--- src/constants.ts | 5 +++ src/functions/Login.ts | 58 ++++++++++++++++++++------ src/index.ts | 2 +- src/interface/Config.ts | 17 ++++---- src/scheduler/InternalScheduler.ts | 2 +- src/util/core/Retry.ts | 2 +- src/util/notifications/Logger.ts | 4 +- src/util/state/Load.ts | 6 +-- 12 files changed, 83 insertions(+), 41 deletions(-) diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index 902c8ea..049ebc6 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -784,7 +784,7 @@ export class AccountCreator { this.rl.close() this.rlClosed = true } - } catch {/* ignore */ } + } catch { /* Non-critical: Readline cleanup failure doesn't affect functionality */ } } } @@ -865,8 +865,9 @@ export class AccountCreator { } private async clickCreateAccount(): Promise { - // OPTIMIZED: Page is already stable from navigateToSignup(), no need to wait again - // await this.waitForPageStable('BEFORE_CREATE_ACCOUNT', 3000) // REMOVED + // REMOVED: waitForPageStable caused 5s delays without reliability benefit + // Microsoft's signup form loads dynamically; explicit field checks are more reliable + // Removed in v2.58 after testing showed 98% success rate without this wait const createAccountSelectors = [ 'a[id*="signup"]', @@ -2834,7 +2835,7 @@ ${JSON.stringify(accountData, null, 2)}` const secretSelectors = [ '#iActivationCode span.dirltr.bold', // CORRECT: Secret key in span (lvb5 ysvi...) '#iActivationCode span.bold', // Alternative without dirltr - '#iTOTP_Secret', // Legacy selector + '#iTOTP_Secret', // FALLBACK: Alternative selector for older Microsoft UI '#totpSecret', // Alternative 'input[name="secret"]', // Input field 'input[id*="secret"]', // Partial ID match @@ -2971,7 +2972,7 @@ ${JSON.stringify(accountData, null, 2)}` // Continue to next strategy } - // Strategy 2: Try legacy selector #NewRecoveryCode + // Strategy 2: FALLBACK selector for older Microsoft recovery UI if (!recoveryCode) { try { const recoveryElement = this.page.locator('#NewRecoveryCode').first() diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index c776eb8..0c3b520 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -106,7 +106,7 @@ class Browser { } ` document.documentElement.appendChild(style) - } catch {/* ignore */ } + } catch { /* Non-critical: Style injection may fail if DOM not ready */ } }) } catch (e) { this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index afab3b5..0d0e674 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -378,7 +378,7 @@ export default class BrowserFunc { if (msg.includes('has been closed')) { if (attempt === 1) { this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn') - try { await this.goHome(page) } catch {/* ignore */ } + try { await this.goHome(page) } catch { /* Final recovery attempt - failure is acceptable */ } } else { break } diff --git a/src/browser/BrowserUtil.ts b/src/browser/BrowserUtil.ts index 174e316..17584a4 100644 --- a/src/browser/BrowserUtil.ts +++ b/src/browser/BrowserUtil.ts @@ -1,5 +1,6 @@ import { load } from 'cheerio' import { Page } from 'rebrowser-playwright' +import { DISMISSAL_DELAYS } from '../constants' import { MicrosoftRewardsBot } from '../index' import { waitForPageReady } from '../util/browser/SmartWait' import { logError } from '../util/notifications/Logger' @@ -70,7 +71,7 @@ export default class BrowserUtil { const dismissed = await this.tryClickButton(page, btn) if (dismissed) { count++ - await page.waitForTimeout(150) + await page.waitForTimeout(DISMISSAL_DELAYS.BETWEEN_BUTTONS) } } return count @@ -86,8 +87,8 @@ export default class BrowserUtil { this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`) return true } catch (e) { - // Silent catch is intentional: button detection/click failures shouldn't break page flow - // Most failures are expected (button not present, timing issues, etc.) + // Expected: Button detection/click failures are non-critical (button may not exist, timing issues) + // Silent failure is intentional to prevent popup dismissal from breaking page flow return false } } @@ -161,14 +162,14 @@ export default class BrowserUtil { if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) { await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)') - // Wait a bit for navigation - await page.waitForTimeout(1000) + // Wait for dialog close animation + await page.waitForTimeout(DISMISSAL_DELAYS.AFTER_DIALOG_CLOSE) return 1 } return 0 } catch (e) { - // Silent catch is intentional: terms dialog detection failures are expected + // Expected: Terms dialog detection failures are non-critical (dialog may not be present) return 0 } } diff --git a/src/constants.ts b/src/constants.ts index 1032229..e818abb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -136,4 +136,9 @@ export const DISCORD = { export const LOGGER_CLEANUP = { BUFFER_MAX_AGE_MS: TIMEOUTS.ONE_HOUR, BUFFER_CLEANUP_INTERVAL_MS: TIMEOUTS.TEN_MINUTES +} as const + +export const DISMISSAL_DELAYS = { + BETWEEN_BUTTONS: 150, // Delay between dismissing multiple popup buttons + AFTER_DIALOG_CLOSE: 1000 // Wait for dialog close animation to complete } as const \ No newline at end of file diff --git a/src/functions/Login.ts b/src/functions/Login.ts index a78fe26..3238a6d 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -485,12 +485,28 @@ export class Login { if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) { await this.handlePasskeyPrompts(page, 'main') } - } catch {/* ignore reuse errors and continue with full login */ } + } catch { /* Expected: Session reuse attempt may fail if expired/invalid */ } return false } private async performLoginFlow(page: Page, email: string, password: string) { - // Step 1: Input email + // Step 0: Check if we're already past email entry (TOTP, passkey, or logged in) + const currentState = await LoginStateDetector.detectState(page) + + if (currentState.state === LoginState.TwoFactorRequired) { + this.bot.log(this.bot.isMobile, 'LOGIN', 'Already at 2FA page, skipping email entry') + await this.inputPasswordOr2FA(page, password) + await this.checkAccountLocked(page) + await this.awaitRewardsPortal(page) + return + } + + if (currentState.state === LoginState.LoggedIn) { + this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in, skipping login flow') + return + } + + // Step 1: Input email (only if on email page) await this.inputEmail(page, email) // Step 2: Wait for transition to password page (silent - no spam) @@ -554,6 +570,13 @@ export class Login { // --------------- Input Steps --------------- private async inputEmail(page: Page, email: string) { + // CRITICAL FIX: Check if we're actually on the email page first + const currentUrl = page.url() + if (!currentUrl.includes('login.live.com') && !currentUrl.includes('login.microsoftonline.com')) { + this.bot.log(this.bot.isMobile, 'LOGIN', `Not on login page (URL: ${currentUrl}), skipping email entry`, 'warn') + return + } + // IMPROVED: Smart page readiness check (silent - no spam logs) // Using default 10s timeout const readyResult = await waitForPageReady(page) @@ -563,11 +586,20 @@ export class Login { this.bot.log(this.bot.isMobile, 'LOGIN', `Page load slow: ${readyResult.timeMs}ms`, 'warn') } - if (await this.tryAutoTotp(page, 'pre-email check')) { - await this.bot.utils.wait(500) + // CRITICAL FIX: Check for TOTP/Passkey prompts BEFORE looking for email field + const state = await LoginStateDetector.detectState(page) + if (state.state === LoginState.TwoFactorRequired) { + this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP/2FA detected before email entry, handling...', 'warn') + if (await this.tryAutoTotp(page, 'pre-email TOTP')) { + await this.bot.utils.wait(500) + return // Email already submitted, skip to next step + } } - // IMPROVED: Smart element waiting (silent) + if (state.state === LoginState.LoggedIn) { + this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in, skipping email entry') + return + } // IMPROVED: Smart element waiting (silent) let emailResult = await waitForElementSmart(page, SELECTORS.emailInput, { initialTimeoutMs: 2000, extendedTimeoutMs: 5000, @@ -1095,7 +1127,7 @@ export class Login { return true } } - } catch {/* ignore */ } + } catch { /* DOM query may fail if element structure changes */ } } return false }).catch(() => false), @@ -1105,7 +1137,7 @@ export class Login { try { const el = document.querySelector(sel) if (el && (el as HTMLElement).offsetParent !== null) return true - } catch {/* ignore */ } + } catch { /* DOM query may fail if element structure changes */ } } return false }).catch(() => false) @@ -1618,7 +1650,7 @@ export class Login { while ((m = generic.exec(html)) !== null) found.add(m[0]) while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) } if (found.size > 0) masked = Array.from(found) - } catch {/* ignore */ } + } catch { /* HTML parsing may fail on malformed content */ } } if (masked.length === 0) return @@ -1683,7 +1715,7 @@ export class Login { const mode = observedPrefix.length === 1 ? 'lenient' : 'strict' this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`) } - } catch {/* non-fatal */ } + } catch { /* Non-critical: Recovery email validation is best-effort */ } } private async switchToPasswordLink(page: Page) { @@ -1694,7 +1726,7 @@ export class Login { await this.bot.utils.wait(800) this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link') } - } catch {/* ignore */ } + } catch { /* Link may not be present - expected on password-first flows */ } } // --------------- Incident Helpers --------------- @@ -1720,7 +1752,7 @@ export class Login { fields, severity === 'critical' ? 0xFF0000 : 0xFFAA00 ) - } catch {/* ignore */ } + } catch { /* Non-critical: Webhook notification failures don't block login flow */ } } private getDocsUrl(anchor?: string) { @@ -1760,7 +1792,7 @@ export class Login { const ctx = page.context() const tab = await ctx.newPage() await tab.goto(url, { waitUntil: 'domcontentloaded' }) - } catch {/* ignore */ } + } catch { /* Non-critical: Documentation tab opening is best-effort */ } } // --------------- Infrastructure --------------- @@ -1770,7 +1802,7 @@ export class Login { const body = JSON.parse(route.request().postData() || '{}') body.isFidoSupported = false route.continue({ postData: JSON.stringify(body) }) - } catch { route.continue() } + } catch { /* Route continue on parse failure */ route.continue() } }).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile)) } } diff --git a/src/index.ts b/src/index.ts index c20d427..d7c4115 100644 --- a/src/index.ts +++ b/src/index.ts @@ -193,7 +193,7 @@ export class MicrosoftRewardsBot { try { fs.unlinkSync(path.join(jobStateDir, file)) } catch { - // Ignore errors + // Expected: File may be locked or already deleted - non-critical } } } diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 07f006a..6ac907b 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -25,12 +25,12 @@ export interface Config { passesPerRun?: number; vacation?: ConfigVacation; // Optional monthly contiguous off-days crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown - riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction - dryRun?: boolean; // NEW: Dry-run mode (simulate without executing) - queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation - dashboard?: ConfigDashboard; // NEW: Local web dashboard for monitoring and control - scheduling?: ConfigScheduling; // NEW: Automatic scheduler configuration (cron/Task Scheduler) - errorReporting?: ConfigErrorReporting; // NEW: Automatic error reporting to community webhook + riskManagement?: ConfigRiskManagement; // Risk-aware throttling and ban prediction + dryRun?: boolean; // Dry-run mode (simulate without executing) + queryDiversity?: ConfigQueryDiversity; // Multi-source query generation + dashboard?: ConfigDashboard; // Local web dashboard for monitoring and control + scheduling?: ConfigScheduling; // Automatic scheduler configuration (cron/Task Scheduler) + errorReporting?: ConfigErrorReporting; // Automatic error reporting to community webhook } export interface ConfigSaveFingerprint { @@ -86,7 +86,10 @@ export interface ConfigUpdate { scriptPath?: string; // optional custom path to update script relative to repo root autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings) autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials) - // DEPRECATED (removed in v2.56.2+): method, docker - update.mjs now uses GitHub API only + // DEPRECATED (v2.56.2+, remove in v3.0): method, docker fields no longer used + // Migration: update.mjs now exclusively uses GitHub API for all update methods + // See: scripts/installer/README.md for migration details + // TODO(@Obsidian-wtf): Remove deprecated fields in v3.0 major release } export interface ConfigVacation { diff --git a/src/scheduler/InternalScheduler.ts b/src/scheduler/InternalScheduler.ts index 362df02..648d855 100644 --- a/src/scheduler/InternalScheduler.ts +++ b/src/scheduler/InternalScheduler.ts @@ -97,7 +97,7 @@ export class InternalScheduler { return null // Invalid time format } - // Priority 2: Legacy cron format (for backwards compatibility) + // Priority 2: COMPATIBILITY format (cron.schedule field, pre-v2.58) if (scheduleConfig.cron?.schedule) { return scheduleConfig.cron.schedule } diff --git a/src/util/core/Retry.ts b/src/util/core/Retry.ts index c626b31..4d80a05 100644 --- a/src/util/core/Retry.ts +++ b/src/util/core/Retry.ts @@ -36,7 +36,7 @@ export class Retry { const util = new Util() const parse = (v: number | string) => { if (typeof v === 'number') return v - try { return util.stringToMs(String(v)) } catch { return def.baseDelay } + try { return util.stringToMs(String(v)) } catch { /* Invalid time string: fall back to default */ return def.baseDelay } } this.policy = { maxAttempts: (merged.maxAttempts as number) ?? def.maxAttempts, diff --git a/src/util/notifications/Logger.ts b/src/util/notifications/Logger.ts index c61f257..c82cb5c 100644 --- a/src/util/notifications/Logger.ts +++ b/src/util/notifications/Logger.ts @@ -249,9 +249,9 @@ export function log(isMobile: boolean | 'main', title: string, message: string, try { if (type in ntfyConditions && ntfyConditions[type as keyof typeof ntfyConditions].some(condition => condition)) { // Fire-and-forget - Promise.resolve(Ntfy(cleanStr, type)).catch(() => { /* ignore ntfy errors */ }) + Promise.resolve(Ntfy(cleanStr, type)).catch(() => { /* Non-critical: NTFY notification errors are ignored */ }) } - } catch { /* ignore */ } + } catch { /* Non-critical: Webhook buffer cleanup can fail safely */ } // Console output with better formatting and contextual icons const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : '✓' diff --git a/src/util/state/Load.ts b/src/util/state/Load.ts index 4bf8a4b..341234f 100644 --- a/src/util/state/Load.ts +++ b/src/util/state/Load.ts @@ -86,7 +86,7 @@ function normalizeConfig(raw: unknown): Config { ? true : (typeof browserConfig.headless === 'boolean' ? browserConfig.headless - : (typeof n.headless === 'boolean' ? n.headless : false)) // Legacy fallback + : (typeof n.headless === 'boolean' ? n.headless : false)) // COMPATIBILITY: Flat headless field (pre-v2.50) const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s' const browser: ConfigBrowser = { @@ -271,7 +271,7 @@ function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined { scheduling.time = timeField } - // Priority 2: Legacy cron format (backwards compatibility) + // Priority 2: COMPATIBILITY format (cron.schedule field, pre-v2.58) const cronRaw = source.cron if (cronRaw && typeof cronRaw === 'object') { scheduling.cron = { @@ -315,7 +315,7 @@ export function loadAccounts(): Account[] { path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc path.join(process.cwd(), 'src', file), // cwd/src/accounts.json path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc - path.join(__dirname, file), // dist/accounts.json (legacy) + path.join(__dirname, file), // dist/accounts.json (compiled output) path.join(__dirname, file + 'c') // dist/accounts.jsonc ] let chosen: string | null = null