fix: Improve error handling and logging across multiple modules; enhance compatibility for legacy formats

This commit is contained in:
2025-11-14 20:56:48 +01:00
parent ec1596bbbd
commit cbd05d128e
12 changed files with 83 additions and 41 deletions

View File

@@ -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<void> {
// 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()

View File

@@ -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')

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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' ? '⚠' : '✓'

View File

@@ -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