mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
feat: Improve error logging and validation across various flows and utilities
This commit is contained in:
@@ -9,7 +9,6 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte
|
||||
import { EarnablePoints } from '../interface/Points'
|
||||
import { QuizData } from '../interface/QuizData'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { logError } from '../util/Logger'
|
||||
|
||||
|
||||
export default class BrowserFunc {
|
||||
@@ -114,7 +113,7 @@ export default class BrowserFunc {
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `[goHome] Navigation failed: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -179,7 +178,7 @@ export default class BrowserFunc {
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Error fetching dashboard data: ${errorMessage}`, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `[getDashboardData] Failed to fetch dashboard data: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -246,7 +245,7 @@ export default class BrowserFunc {
|
||||
|
||||
/**
|
||||
* Parse dashboard object from script content
|
||||
* FIXED: Added format validation before JSON.parse
|
||||
* IMPROVED: Enhanced validation with structure checks
|
||||
*/
|
||||
private async parseDashboardFromScript(page: Page, scriptContent: string): Promise<DashboardData | null> {
|
||||
return await page.evaluate((scriptContent: string) => {
|
||||
@@ -262,16 +261,27 @@ export default class BrowserFunc {
|
||||
try {
|
||||
const jsonStr = match[1]
|
||||
// Validate basic JSON structure before parsing
|
||||
if (!jsonStr.trim().startsWith('{') || !jsonStr.trim().endsWith('}')) {
|
||||
const trimmed = jsonStr.trim()
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
// Validate it's actually an object
|
||||
|
||||
// Enhanced validation: check structure and type
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate essential dashboard properties exist
|
||||
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
|
||||
continue
|
||||
}
|
||||
|
||||
// Successfully validated dashboard structure
|
||||
return parsed
|
||||
} catch (e) {
|
||||
// JSON.parse failed or validation error - try next pattern
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -339,7 +349,7 @@ export default class BrowserFunc {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', `[getBrowserEarnablePoints] Failed to calculate earnable points: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -404,7 +414,7 @@ export default class BrowserFunc {
|
||||
return points
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', `[getAppEarnablePoints] Failed to fetch app earnable points: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -420,7 +430,7 @@ export default class BrowserFunc {
|
||||
return data.userStatus.availablePoints
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', `[getCurrentPoints] Failed to fetch current points: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -493,7 +503,7 @@ export default class BrowserFunc {
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `[getQuizData] Failed to extract quiz data: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -561,7 +571,7 @@ export default class BrowserFunc {
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred: ' + errorMessage, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `[closeBrowser] Failed to close browser cleanly: ${errorMessage}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
/**
|
||||
* Parse environment variable as number with validation
|
||||
* FIXED: Added strict validation for min/max boundaries
|
||||
* FIXED: Added strict validation for min/max boundaries with logging
|
||||
* @param key Environment variable name
|
||||
* @param defaultValue Default value if parsing fails or out of range
|
||||
* @param min Minimum allowed value
|
||||
@@ -18,13 +18,24 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
|
||||
|
||||
const parsed = Number(raw)
|
||||
// Strict validation: must be finite, not NaN, and within bounds
|
||||
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
||||
if (!Number.isFinite(parsed)) {
|
||||
console.warn(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if (parsed < min || parsed > max) {
|
||||
console.warn(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// Login timeout boundaries (in milliseconds)
|
||||
const LOGIN_TIMEOUT_MIN_MS = 30000 // 30 seconds - minimum login wait
|
||||
const LOGIN_TIMEOUT_MAX_MS = 600000 // 10 minutes - maximum login wait
|
||||
const LOGIN_TIMEOUT_DEFAULT_MS = 180000 // 3 minutes - default login timeout
|
||||
|
||||
export const TIMEOUTS = {
|
||||
SHORT: 500,
|
||||
MEDIUM: 1500,
|
||||
@@ -33,7 +44,7 @@ export const TIMEOUTS = {
|
||||
VERY_LONG: 5000,
|
||||
EXTRA_LONG: 10000,
|
||||
DASHBOARD_WAIT: 10000,
|
||||
LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', 180000, 30000, 600000),
|
||||
LOGIN_MAX: parseEnvNumber('LOGIN_MAX_WAIT_MS', LOGIN_TIMEOUT_DEFAULT_MS, LOGIN_TIMEOUT_MIN_MS, LOGIN_TIMEOUT_MAX_MS),
|
||||
NETWORK_IDLE: 5000,
|
||||
ONE_MINUTE: 60000,
|
||||
ONE_HOUR: 3600000,
|
||||
|
||||
@@ -29,8 +29,23 @@ export class DesktopFlow {
|
||||
|
||||
/**
|
||||
* Execute the full desktop automation flow for an account
|
||||
* @param account Account to process
|
||||
* @returns Points collected during the flow
|
||||
*
|
||||
* Performs the following tasks in sequence:
|
||||
* 1. Browser initialization with fingerprinting
|
||||
* 2. Microsoft account login with 2FA support
|
||||
* 3. Daily set completion
|
||||
* 4. More promotions (quizzes, polls, etc.)
|
||||
* 5. Punch cards
|
||||
* 6. Desktop searches
|
||||
*
|
||||
* @param account Account to process (email, password, totp, proxy)
|
||||
* @returns Promise resolving to points collected during the flow
|
||||
* @throws {Error} If critical operation fails (login, browser init)
|
||||
*
|
||||
* @example
|
||||
* const flow = new DesktopFlow(bot)
|
||||
* const result = await flow.run(account)
|
||||
* console.log(`Collected ${result.collectedPoints} points`)
|
||||
*/
|
||||
async run(account: Account): Promise<DesktopFlowResult> {
|
||||
this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow')
|
||||
@@ -63,7 +78,10 @@ export class DesktopFlow {
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
} catch {/* ignore */}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(false, 'DESKTOP-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
// Save session for convenience, but do not close the browser
|
||||
try {
|
||||
|
||||
@@ -31,9 +31,24 @@ export class MobileFlow {
|
||||
|
||||
/**
|
||||
* Execute the full mobile automation flow for an account
|
||||
* @param account Account to process
|
||||
* @param retryTracker Retry tracker for mobile search failures
|
||||
* @returns Points collected during the flow
|
||||
*
|
||||
* Performs the following tasks in sequence:
|
||||
* 1. Mobile browser initialization with mobile user agent
|
||||
* 2. Microsoft account login
|
||||
* 3. OAuth token acquisition for mobile API access
|
||||
* 4. Daily check-in via mobile API
|
||||
* 5. Read to earn articles
|
||||
* 6. Mobile searches with retry logic
|
||||
*
|
||||
* @param account Account to process (email, password, totp, proxy)
|
||||
* @param retryTracker Retry tracker for mobile search failures (auto-created if not provided)
|
||||
* @returns Promise resolving to points collected during mobile flow
|
||||
* @throws {Error} If critical operation fails (login, OAuth)
|
||||
*
|
||||
* @example
|
||||
* const flow = new MobileFlow(bot)
|
||||
* const result = await flow.run(account)
|
||||
* console.log(`Mobile: ${result.collectedPoints} points`)
|
||||
*/
|
||||
async run(
|
||||
account: Account,
|
||||
@@ -69,7 +84,10 @@ export class MobileFlow {
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
} catch {/* ignore */}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(true, 'MOBILE-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, true)
|
||||
|
||||
@@ -39,6 +39,24 @@ export class Activities {
|
||||
}
|
||||
|
||||
// Centralized dispatcher for activities from dashboard/punchcards
|
||||
/**
|
||||
* Execute a promotional activity (quiz, poll, search-on-bing, etc.)
|
||||
*
|
||||
* Automatically detects activity type and delegates to specialized handler:
|
||||
* - quiz → Quiz handler
|
||||
* - abc → ABC (drag-and-drop) handler
|
||||
* - thisorthat → This or That handler
|
||||
* - poll → Poll handler
|
||||
* - urlreward → URL reward handler
|
||||
*
|
||||
* @param page Playwright page for activity execution
|
||||
* @param activity Activity metadata from dashboard data
|
||||
* @returns Promise resolving when activity is complete
|
||||
* @throws {Error} If activity type is unsupported or execution fails
|
||||
*
|
||||
* @example
|
||||
* await activities.run(page, dailySetActivity)
|
||||
*/
|
||||
async run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void> {
|
||||
// First, try custom handlers (if any)
|
||||
for (const h of this.handlers) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { platform } from 'os'
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import { Counters, DashboardData } from '../../interface/DashboardData'
|
||||
import { GoogleSearch } from '../../interface/Search'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
|
||||
type GoogleTrendsResponse = [
|
||||
string,
|
||||
@@ -16,6 +16,10 @@ type GoogleTrendsResponse = [
|
||||
][]
|
||||
];
|
||||
|
||||
// Search stagnation thresholds (magic numbers extracted as constants)
|
||||
const MOBILE_STAGNATION_LIMIT = 5 // Mobile searches: abort after 5 queries without points
|
||||
const DESKTOP_STAGNATION_LIMIT = 10 // Desktop searches: abort after 10 queries without points
|
||||
|
||||
export class Search extends Workers {
|
||||
private bingHome = 'https://bing.com'
|
||||
private searchPageURL = ''
|
||||
@@ -119,15 +123,15 @@ export class Search extends Workers {
|
||||
|
||||
if (missingPoints === 0) break
|
||||
|
||||
// Only for mobile searches
|
||||
if (stagnation > 5 && this.bot.isMobile) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
|
||||
// Only for mobile searches - abort early if User-Agent is likely incorrect
|
||||
if (stagnation > MOBILE_STAGNATION_LIMIT && this.bot.isMobile) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${MOBILE_STAGNATION_LIMIT} iterations, likely bad User-Agent`, 'warn')
|
||||
break
|
||||
}
|
||||
|
||||
// If we didn't gain points for 10 iterations, assume it's stuck
|
||||
if (stagnation > 10) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
|
||||
// If we didn't gain points for many iterations, assume it's stuck
|
||||
if (stagnation > DESKTOP_STAGNATION_LIMIT) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search didn't gain points for ${DESKTOP_STAGNATION_LIMIT} iterations, aborting searches`, 'warn')
|
||||
stagnation = 0 // allow fallback loop below
|
||||
break
|
||||
}
|
||||
|
||||
@@ -139,6 +139,17 @@ function determineColorFromContent(content: string): number {
|
||||
return DISCORD.COLOR_GRAY
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if config has valid logging configuration
|
||||
*/
|
||||
function hasValidLogging(config: unknown): config is { logging: { excludeFunc?: string[]; webhookExcludeFunc?: string[] } } {
|
||||
return typeof config === 'object' &&
|
||||
config !== null &&
|
||||
'logging' in config &&
|
||||
typeof config.logging === 'object' &&
|
||||
config.logging !== null
|
||||
}
|
||||
|
||||
function enqueueWebhookLog(url: string, line: string) {
|
||||
const buf = getBuffer(url)
|
||||
buf.lines.push(line)
|
||||
@@ -164,9 +175,8 @@ function enqueueWebhookLog(url: string, line: string) {
|
||||
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void {
|
||||
const configData = loadConfig()
|
||||
|
||||
// Access logging config with fallback for backward compatibility
|
||||
const configAny = configData as unknown as Record<string, unknown>
|
||||
const logging = configAny.logging as { excludeFunc?: string[]; logExcludeFunc?: string[] } | undefined
|
||||
// Access logging config with type guard for safer access
|
||||
const logging = hasValidLogging(configData) ? configData.logging : undefined
|
||||
const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? []
|
||||
|
||||
if (logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) {
|
||||
@@ -178,7 +188,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
|
||||
// Clean string for notifications (no chalk, structured)
|
||||
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
|
||||
const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg
|
||||
const loggingCfg: LoggingCfg = logging || {}
|
||||
const shouldRedact = !!loggingCfg.redactEmails
|
||||
const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => {
|
||||
const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}`
|
||||
@@ -269,7 +279,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
|
||||
// Webhook streaming (live logs)
|
||||
try {
|
||||
const loggingCfg: Record<string, unknown> = (configAny.logging || {}) as Record<string, unknown>
|
||||
const loggingCfg: Record<string, unknown> = (logging || {}) as Record<string, unknown>
|
||||
const webhookCfg = configData.webhook
|
||||
const liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : ''
|
||||
const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '')
|
||||
|
||||
@@ -123,6 +123,11 @@ export class Util {
|
||||
return []
|
||||
}
|
||||
|
||||
// Check for undefined/null elements which could cause issues downstream
|
||||
if (arr.some(item => item === undefined || item === null)) {
|
||||
throw new Error('Array contains undefined or null elements which are not allowed.')
|
||||
}
|
||||
|
||||
if (!Number.isFinite(numChunks) || numChunks <= 0) {
|
||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user