mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
fix: Improve error handling and logging across multiple modules; enhance compatibility for legacy formats
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' ? '⚠' : '✓'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user