mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
feat: implement error logging in catch blocks for improved error handling and debugging
This commit is contained in:
@@ -9,6 +9,7 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte
|
||||
import { QuizData } from '../interface/QuizData'
|
||||
import { AppUserData } from '../interface/AppUserData'
|
||||
import { EarnablePoints } from '../interface/Points'
|
||||
import { logError } from '../util/Logger'
|
||||
|
||||
|
||||
export default class BrowserFunc {
|
||||
@@ -148,7 +149,7 @@ export default class BrowserFunc {
|
||||
|
||||
// Force a navigation retry once before failing hard
|
||||
await this.goHome(target)
|
||||
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(() => {})
|
||||
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(logError('BROWSER-FUNC', 'Dashboard recovery load failed', this.bot.isMobile))
|
||||
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
|
||||
|
||||
scriptContent = await this.extractDashboardScript(target)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { load } from 'cheerio'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { logError } from '../util/Logger'
|
||||
|
||||
type DismissButton = { selector: string; label: string; isXPath?: boolean }
|
||||
|
||||
@@ -80,7 +81,7 @@ export default class BrowserUtil {
|
||||
const visible = await loc.first().isVisible({ timeout: 200 }).catch(() => false)
|
||||
if (!visible) return false
|
||||
|
||||
await loc.first().click({ timeout: 500 }).catch(() => {})
|
||||
await loc.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', `Failed to click ${btn.label}`, this.bot.isMobile))
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
|
||||
return true
|
||||
} catch {
|
||||
@@ -97,14 +98,14 @@ export default class BrowserUtil {
|
||||
|
||||
const rejectBtn = overlay.locator(reject)
|
||||
if (await rejectBtn.first().isVisible().catch(() => false)) {
|
||||
await rejectBtn.first().click({ timeout: 500 }).catch(() => {})
|
||||
await rejectBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay reject click failed', this.bot.isMobile))
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
|
||||
return 1
|
||||
}
|
||||
|
||||
const acceptBtn = overlay.locator(accept)
|
||||
if (await acceptBtn.first().isVisible().catch(() => false)) {
|
||||
await acceptBtn.first().click({ timeout: 500 }).catch(() => {})
|
||||
await acceptBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay accept click failed', this.bot.isMobile))
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
|
||||
return 1
|
||||
}
|
||||
@@ -124,12 +125,12 @@ export default class BrowserUtil {
|
||||
|
||||
const closeBtn = dialog.locator(closeButtons).first()
|
||||
if (await closeBtn.isVisible({ timeout: 200 }).catch(() => false)) {
|
||||
await closeBtn.click({ timeout: 500 }).catch(() => {})
|
||||
await closeBtn.click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Streak dialog close failed', this.bot.isMobile))
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Button')
|
||||
return 1
|
||||
}
|
||||
|
||||
await page.keyboard.press('Escape').catch(() => {})
|
||||
await page.keyboard.press('Escape').catch(logError('BROWSER-UTIL', 'Streak dialog Escape failed', this.bot.isMobile))
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Escape')
|
||||
return 1
|
||||
} catch {
|
||||
@@ -153,7 +154,7 @@ export default class BrowserUtil {
|
||||
// Click the Next button
|
||||
const nextBtn = page.locator(nextButton).first()
|
||||
if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
||||
await nextBtn.click({ timeout: 1000 }).catch(() => {})
|
||||
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)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MicrosoftRewardsBot } from '../index'
|
||||
import { OAuth } from '../interface/OAuth'
|
||||
import { Retry } from '../util/Retry'
|
||||
import { LoginState, LoginStateDetector } from '../util/LoginStateDetector'
|
||||
import { logError } from '../util/Logger'
|
||||
|
||||
// -------------------------------
|
||||
// Constants / Tunables
|
||||
@@ -243,7 +244,7 @@ export class Login {
|
||||
const homeUrl = 'https://rewards.bing.com/'
|
||||
try {
|
||||
await page.goto(homeUrl)
|
||||
await page.waitForLoadState('domcontentloaded').catch(()=>{})
|
||||
await page.waitForLoadState('domcontentloaded').catch(logError('LOGIN', 'DOMContentLoaded timeout', this.bot.isMobile))
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
await this.bot.utils.wait(250)
|
||||
|
||||
@@ -384,7 +385,10 @@ export class Login {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled')
|
||||
}
|
||||
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
|
||||
if (next) { await next.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
|
||||
if (next) {
|
||||
await next.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Email submit click failed: ${e}`, 'warn'))
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email')
|
||||
}
|
||||
}
|
||||
|
||||
private async inputPasswordOr2FA(page: Page, password: string) {
|
||||
@@ -394,7 +398,10 @@ export class Login {
|
||||
|
||||
// Some flows require switching to password first
|
||||
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null)
|
||||
if (switchBtn) { await switchBtn.click().catch(()=>{}); await this.bot.utils.wait(1000) }
|
||||
if (switchBtn) {
|
||||
await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn'))
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
// Early TOTP check - if totpSecret is configured, check for TOTP challenge before password
|
||||
if (this.currentTotpSecret) {
|
||||
@@ -429,7 +436,10 @@ export class Login {
|
||||
await page.fill(SELECTORS.passwordInput, '')
|
||||
await page.fill(SELECTORS.passwordInput, password)
|
||||
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
|
||||
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
|
||||
if (submit) {
|
||||
await submit.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Password submit failed: ${e}`, 'warn'))
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted')
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- 2FA Handling ---------------
|
||||
@@ -462,10 +472,10 @@ export class Login {
|
||||
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null)
|
||||
if (!resend) break
|
||||
await this.bot.utils.wait(60000)
|
||||
await resend.click().catch(()=>{})
|
||||
await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile))
|
||||
}
|
||||
}
|
||||
await page.click('button[aria-describedby="confirmSendTitle"]').catch(()=>{})
|
||||
await page.click('button[aria-describedby="confirmSendTitle"]').catch(logError('LOGIN', 'Confirm send click failed', this.bot.isMobile))
|
||||
await this.bot.utils.wait(1500)
|
||||
try {
|
||||
const el = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { timeout: 2000 })
|
||||
@@ -484,7 +494,7 @@ export class Login {
|
||||
} catch {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired – refreshing')
|
||||
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null)
|
||||
if (retryBtn) await retryBtn.click().catch(()=>{})
|
||||
if (retryBtn) await retryBtn.click().catch(logError('LOGIN-AUTH', 'Refresh button click failed', this.bot.isMobile))
|
||||
const refreshed = await this.fetchAuthenticatorNumber(page)
|
||||
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
|
||||
numberToPress = refreshed
|
||||
@@ -584,9 +594,9 @@ export class Login {
|
||||
// Use unified selector system
|
||||
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
|
||||
if (submit) {
|
||||
await submit.click().catch(()=>{})
|
||||
await submit.click().catch(logError('LOGIN-TOTP', 'Auto-submit click failed', this.bot.isMobile))
|
||||
} else {
|
||||
await page.keyboard.press('Enter').catch(()=>{})
|
||||
await page.keyboard.press('Enter').catch(logError('LOGIN-TOTP', 'Auto-submit Enter failed', this.bot.isMobile))
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
|
||||
} catch (error) {
|
||||
@@ -747,7 +757,7 @@ export class Login {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
await loc.click().catch(()=>{})
|
||||
await loc.click().catch(logError('LOGIN', `Click failed for selector: ${sel}`, this.bot.isMobile))
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1027,7 @@ export class Login {
|
||||
const text = (await skipBtn.textContent() || '').trim()
|
||||
// Check if it's actually a skip button (could be other secondary buttons)
|
||||
if (/skip|later|not now|non merci|pas maintenant/i.test(text)) {
|
||||
await skipBtn.click().catch(()=>{})
|
||||
await skipBtn.click().catch(logError('LOGIN-PASSKEY', 'Skip button click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('data-testid secondaryButton')
|
||||
}
|
||||
@@ -1028,7 +1038,11 @@ export class Login {
|
||||
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null)
|
||||
if (biometric) {
|
||||
const btn = await page.$(SELECTORS.passkeySecondary)
|
||||
if (btn) { await btn.click().catch(()=>{}); did = true; this.logPasskeyOnce('video heuristic') }
|
||||
if (btn) {
|
||||
await btn.click().catch(logError('LOGIN-PASSKEY', 'Video heuristic click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('video heuristic')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,11 +1053,17 @@ export class Login {
|
||||
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null)
|
||||
const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
|
||||
const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title)
|
||||
if (looksLike && secBtn) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('title heuristic '+title) }
|
||||
if (looksLike && secBtn) {
|
||||
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('title heuristic '+title)
|
||||
}
|
||||
else if (!did && secBtn && primBtn) {
|
||||
const text = (await secBtn.textContent()||'').trim()
|
||||
if (/skip for now|not now|later|passer|plus tard/i.test(text)) {
|
||||
await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('secondary button text')
|
||||
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('secondary button text')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1051,7 +1071,11 @@ export class Login {
|
||||
// Priority 4: XPath fallback (includes Windows Hello specific patterns)
|
||||
if (!did) {
|
||||
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first()
|
||||
if (await textBtn.isVisible().catch(()=>false)) { await textBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('xpath fallback') }
|
||||
if (await textBtn.isVisible().catch(()=>false)) {
|
||||
await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('xpath fallback')
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4.5: Windows Hello specific detection
|
||||
@@ -1070,7 +1094,7 @@ export class Login {
|
||||
for (const pattern of skipPatterns) {
|
||||
const btn = await page.locator(pattern).first()
|
||||
if (await btn.isVisible().catch(() => false)) {
|
||||
await btn.click().catch(() => {})
|
||||
await btn.click().catch(logError('LOGIN-PASSKEY', 'Windows Hello skip failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('Windows Hello skip')
|
||||
break
|
||||
@@ -1082,14 +1106,22 @@ export class Login {
|
||||
// Priority 5: Close button fallback
|
||||
if (!did) {
|
||||
const close = await page.$('#close-button')
|
||||
if (close) { await close.click().catch(()=>{}); did = true; this.logPasskeyOnce('close button') }
|
||||
if (close) {
|
||||
await close.click().catch(logError('LOGIN-PASSKEY', 'Close button fallback failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('close button')
|
||||
}
|
||||
}
|
||||
|
||||
// KMSI prompt
|
||||
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null)
|
||||
if (kmsi) {
|
||||
const yes = await page.$(SELECTORS.passkeyPrimary)
|
||||
if (yes) { await yes.click().catch(()=>{}); did = true; this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') }
|
||||
if (yes) {
|
||||
await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt')
|
||||
}
|
||||
}
|
||||
|
||||
if (!did && context === 'main') {
|
||||
@@ -1140,9 +1172,9 @@ export class Login {
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'sign-in-blocked'
|
||||
this.startCompromisedInterval()
|
||||
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(()=>{})
|
||||
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(logError('LOGIN-SECURITY', 'Global standby engagement failed', this.bot.isMobile))
|
||||
// Open security docs for immediate guidance (best-effort)
|
||||
await this.openDocsTab(page, docsUrl).catch(()=>{})
|
||||
await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-SECURITY', 'Failed to open docs tab', this.bot.isMobile))
|
||||
return true
|
||||
} catch { return false }
|
||||
}
|
||||
@@ -1250,8 +1282,8 @@ export class Login {
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'recovery-mismatch'
|
||||
this.startCompromisedInterval()
|
||||
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(()=>{})
|
||||
await this.openDocsTab(page, docsUrl).catch(()=>{})
|
||||
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(logError('LOGIN-RECOVERY', 'Global standby failed', this.bot.isMobile))
|
||||
await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-RECOVERY', 'Failed to open docs tab', this.bot.isMobile))
|
||||
} else {
|
||||
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
|
||||
this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
|
||||
@@ -1263,7 +1295,7 @@ export class Login {
|
||||
try {
|
||||
const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first()
|
||||
if (await link.isVisible().catch(()=>false)) {
|
||||
await link.click().catch(()=>{})
|
||||
await link.click().catch(logError('LOGIN', 'Use password link click failed', this.bot.isMobile))
|
||||
await this.bot.utils.wait(800)
|
||||
this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link')
|
||||
}
|
||||
@@ -1335,6 +1367,6 @@ export class Login {
|
||||
body.isFidoSupported = false
|
||||
route.continue({ postData: JSON.stringify(body) })
|
||||
} catch { route.continue() }
|
||||
}).catch(()=>{})
|
||||
}).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MicrosoftRewardsBot } from '../index'
|
||||
import JobState from '../util/JobState'
|
||||
import { Retry } from '../util/Retry'
|
||||
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
|
||||
import { logError } from '../util/Logger'
|
||||
|
||||
export class Workers {
|
||||
public bot: MicrosoftRewardsBot
|
||||
@@ -204,7 +205,7 @@ export class Workers {
|
||||
}
|
||||
|
||||
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile))
|
||||
await this.bot.browser.utils.humanizePage(page)
|
||||
await this.applyThrottle(throttle, 1200, 2600)
|
||||
}
|
||||
|
||||
12
src/index.ts
12
src/index.ts
@@ -120,10 +120,16 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
// Run comprehensive startup validation
|
||||
const validator = new StartupValidator()
|
||||
await validator.validate(this.config, this.accounts)
|
||||
try {
|
||||
await validator.validate(this.config, this.accounts)
|
||||
} catch (error) {
|
||||
// Critical validation errors prevent startup
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
log('main', 'VALIDATION', `Fatal validation error: ${errorMsg}`, 'error')
|
||||
throw error // Re-throw to stop execution
|
||||
}
|
||||
|
||||
// Always continue - validation is informative, not blocking
|
||||
// This allows users to proceed even with warnings or minor issues
|
||||
// Validation passed - continue with initialization
|
||||
|
||||
// Initialize job state
|
||||
if (this.config.jobState?.enabled !== false) {
|
||||
|
||||
@@ -7,6 +7,18 @@ import { DISCORD } from '../constants'
|
||||
|
||||
const WEBHOOK_USERNAME = 'MS Rewards - Live Logs'
|
||||
|
||||
/**
|
||||
* Safe error logger for catch blocks
|
||||
* Use in .catch() to log errors without breaking flow
|
||||
* @example await action().catch(logError('ACTION', 'Failed to do something'))
|
||||
*/
|
||||
export function logError(title: string, message: string, isMobile: boolean | 'main' = 'main') {
|
||||
return (error: unknown) => {
|
||||
const errMsg = error instanceof Error ? error.message : String(error)
|
||||
log(isMobile, title, `${message}: ${errMsg}`, 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
type WebhookBuffer = {
|
||||
lines: string[]
|
||||
sending: boolean
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ValidationError {
|
||||
message: string
|
||||
fix?: string
|
||||
docsLink?: string
|
||||
blocking?: boolean // If true, prevents bot startup
|
||||
}
|
||||
|
||||
export class StartupValidator {
|
||||
@@ -19,8 +20,8 @@ export class StartupValidator {
|
||||
|
||||
/**
|
||||
* Run all validation checks before starting the bot.
|
||||
* Always returns true - validation is informative, not blocking.
|
||||
* Displays errors and warnings but lets execution continue.
|
||||
* Throws ValidationError if critical (blocking) errors are found.
|
||||
* Displays errors and warnings to help users fix configuration issues.
|
||||
*/
|
||||
async validate(config: Config, accounts: Account[]): Promise<boolean> {
|
||||
log('main', 'STARTUP', 'Running configuration validation...')
|
||||
@@ -40,8 +41,15 @@ export class StartupValidator {
|
||||
// Display results (await to respect the delay)
|
||||
await this.displayResults()
|
||||
|
||||
// Always return true - validation is informative only
|
||||
// Users can proceed even with errors (they might be false positives)
|
||||
// Check for blocking errors
|
||||
const blockingErrors = this.errors.filter(e => e.blocking === true)
|
||||
if (blockingErrors.length > 0) {
|
||||
const errorMsg = `Validation failed with ${blockingErrors.length} critical error(s). Fix configuration before proceeding.`
|
||||
log('main', 'VALIDATION', errorMsg, 'error')
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
// Non-blocking errors and warnings allow execution to continue
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -51,7 +59,8 @@ export class StartupValidator {
|
||||
'accounts',
|
||||
'No accounts found in accounts.json',
|
||||
'Add at least one account to src/accounts.json or src/accounts.jsonc',
|
||||
'docs/accounts.md'
|
||||
'docs/accounts.md',
|
||||
true // blocking: no accounts = nothing to run
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -64,13 +73,17 @@ export class StartupValidator {
|
||||
this.addError(
|
||||
'accounts',
|
||||
`${prefix}: Missing or invalid email address`,
|
||||
'Add a valid email address in the "email" field'
|
||||
'Add a valid email address in the "email" field',
|
||||
undefined,
|
||||
true // blocking: email is required
|
||||
)
|
||||
} else if (!/@/.test(account.email)) {
|
||||
this.addError(
|
||||
'accounts',
|
||||
`${prefix}: Email format is invalid`,
|
||||
'Email must contain @ symbol (e.g., user@example.com)'
|
||||
'Email must contain @ symbol (e.g., user@example.com)',
|
||||
undefined,
|
||||
true // blocking: invalid email = cannot login
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,7 +92,9 @@ export class StartupValidator {
|
||||
this.addError(
|
||||
'accounts',
|
||||
`${prefix}: Missing or invalid password`,
|
||||
'Add your Microsoft account password in the "password" field'
|
||||
'Add your Microsoft account password in the "password" field',
|
||||
undefined,
|
||||
true // blocking: password is required
|
||||
)
|
||||
} else if (account.password.length < 4) {
|
||||
this.addWarning(
|
||||
@@ -218,7 +233,9 @@ export class StartupValidator {
|
||||
this.addError(
|
||||
'config',
|
||||
'Global timeout is set to 0',
|
||||
'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs'
|
||||
'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs',
|
||||
undefined,
|
||||
true // blocking: 0 timeout = infinite hangs guaranteed
|
||||
)
|
||||
}
|
||||
|
||||
@@ -368,13 +385,16 @@ export class StartupValidator {
|
||||
'network',
|
||||
'Webhook enabled but URL is missing',
|
||||
'Add webhook URL or set webhook.enabled=false',
|
||||
'docs/config.md'
|
||||
'docs/config.md',
|
||||
true // blocking: enabled but no URL = will crash
|
||||
)
|
||||
} else if (!config.webhook.url.startsWith('http')) {
|
||||
this.addError(
|
||||
'network',
|
||||
`Invalid webhook URL: ${config.webhook.url}`,
|
||||
'Webhook URL must start with http:// or https://'
|
||||
'Webhook URL must start with http:// or https://',
|
||||
undefined,
|
||||
true // blocking: invalid URL = will crash
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -385,7 +405,9 @@ export class StartupValidator {
|
||||
this.addError(
|
||||
'network',
|
||||
'Conclusion webhook enabled but URL is missing',
|
||||
'Add conclusion webhook URL or disable it'
|
||||
'Add conclusion webhook URL or disable it',
|
||||
undefined,
|
||||
true // blocking: enabled but no URL = will crash
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -397,7 +419,8 @@ export class StartupValidator {
|
||||
'network',
|
||||
'NTFY enabled but URL is missing',
|
||||
'Add NTFY server URL or set ntfy.enabled=false',
|
||||
'docs/ntfy.md'
|
||||
'docs/ntfy.md',
|
||||
true // blocking: enabled but no URL = will crash
|
||||
)
|
||||
}
|
||||
if (!config.ntfy.topic || config.ntfy.topic.trim() === '') {
|
||||
@@ -405,7 +428,8 @@ export class StartupValidator {
|
||||
'network',
|
||||
'NTFY enabled but topic is missing',
|
||||
'Add NTFY topic name',
|
||||
'docs/ntfy.md'
|
||||
'docs/ntfy.md',
|
||||
true // blocking: enabled but no topic = will crash
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -611,12 +635,12 @@ export class StartupValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private addError(category: string, message: string, fix?: string, docsLink?: string): void {
|
||||
this.errors.push({ severity: 'error', category, message, fix, docsLink })
|
||||
private addError(category: string, message: string, fix?: string, docsLink?: string, blocking = false): void {
|
||||
this.errors.push({ severity: 'error', category, message, fix, docsLink, blocking })
|
||||
}
|
||||
|
||||
private addWarning(category: string, message: string, fix?: string, docsLink?: string): void {
|
||||
this.warnings.push({ severity: 'warning', category, message, fix, docsLink })
|
||||
this.warnings.push({ severity: 'warning', category, message, fix, docsLink, blocking: false })
|
||||
}
|
||||
|
||||
private async displayResults(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user