mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
Improved error handling and added protection against race conditions in the bot controller. Extracted Microsoft domain verification for better maintainability. Added support for custom activity managers and improved documentation.
This commit is contained in:
@@ -33,6 +33,18 @@ export class AccountCreator {
|
||||
await this.page.waitForTimeout(Math.floor(delay))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if email domain is a Microsoft-managed domain
|
||||
* Extracted to avoid duplication and improve maintainability
|
||||
*/
|
||||
private isMicrosoftDomain(domain: string | undefined): boolean {
|
||||
if (!domain) return false
|
||||
const lowerDomain = domain.toLowerCase()
|
||||
return lowerDomain === 'outlook.com' ||
|
||||
lowerDomain === 'hotmail.com' ||
|
||||
lowerDomain === 'outlook.fr'
|
||||
}
|
||||
|
||||
/**
|
||||
* UTILITY: Find first visible element from list of selectors
|
||||
* Reserved for future use - simplifies selector fallback logic
|
||||
@@ -950,12 +962,16 @@ export class AccountCreator {
|
||||
await emailInput.fill(newEmail)
|
||||
await this.humanDelay(1200, 2500)
|
||||
|
||||
// SMART VERIFICATION: Microsoft may separate domain
|
||||
// SMART VERIFICATION: Microsoft may separate domain for managed email providers
|
||||
const inputValue = await emailInput.inputValue().catch(() => '')
|
||||
const emailUsername = newEmail.split('@')[0]
|
||||
const emailDomain = newEmail.split('@')[1]
|
||||
|
||||
if (inputValue === newEmail || (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr'))) {
|
||||
// Check if input matches full email OR username only (when domain is Microsoft-managed)
|
||||
const isFullMatch = inputValue === newEmail
|
||||
const isUsernameOnlyMatch = inputValue === emailUsername && this.isMicrosoftDomain(emailDomain)
|
||||
|
||||
if (isFullMatch || isUsernameOnlyMatch) {
|
||||
return true
|
||||
} else {
|
||||
throw new Error('Email retry input value not verified')
|
||||
@@ -1024,12 +1040,16 @@ export class AccountCreator {
|
||||
await emailInput.fill(newEmail)
|
||||
await this.humanDelay(1200, 2500)
|
||||
|
||||
// SMART VERIFICATION: Microsoft may separate domain
|
||||
// SMART VERIFICATION: Microsoft may separate domain for managed email providers
|
||||
const inputValue = await emailInput.inputValue().catch(() => '')
|
||||
const emailUsername = newEmail.split('@')[0]
|
||||
const emailDomain = newEmail.split('@')[1]
|
||||
|
||||
if (inputValue === newEmail || (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr'))) {
|
||||
// Check if input matches full email OR username only (when domain is Microsoft-managed)
|
||||
const isFullMatch = inputValue === newEmail
|
||||
const isUsernameOnlyMatch = inputValue === emailUsername && this.isMicrosoftDomain(emailDomain)
|
||||
|
||||
if (isFullMatch || isUsernameOnlyMatch) {
|
||||
return true
|
||||
} else {
|
||||
throw new Error('Email auto-retry input value not verified')
|
||||
|
||||
@@ -6,6 +6,7 @@ import { dashboardState } from './state'
|
||||
export class BotController {
|
||||
private botInstance: MicrosoftRewardsBot | null = null
|
||||
private startTime?: Date
|
||||
private isStarting: boolean = false // Race condition protection
|
||||
|
||||
constructor() {
|
||||
process.on('exit', () => this.stop())
|
||||
@@ -24,11 +25,17 @@ export class BotController {
|
||||
}
|
||||
|
||||
public async start(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||
// FIXED: Race condition protection - prevent multiple simultaneous start() calls
|
||||
if (this.botInstance) {
|
||||
return { success: false, error: 'Bot is already running' }
|
||||
}
|
||||
|
||||
if (this.isStarting) {
|
||||
return { success: false, error: 'Bot is currently starting, please wait' }
|
||||
}
|
||||
|
||||
try {
|
||||
this.isStarting = true
|
||||
this.log('🚀 Starting bot...', 'log')
|
||||
|
||||
const { MicrosoftRewardsBot } = await import('../index')
|
||||
@@ -61,6 +68,8 @@ export class BotController {
|
||||
this.log(`Failed to start bot: ${errorMsg}`, 'error')
|
||||
this.cleanup()
|
||||
return { success: false, error: errorMsg }
|
||||
} finally {
|
||||
this.isStarting = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { TIMEOUTS } from '../constants'
|
||||
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
@@ -14,6 +15,14 @@ const ACTIVITY_SELECTORS = {
|
||||
byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .pointLink:not(.contentContainer .pointLink)`
|
||||
} as const
|
||||
|
||||
// Activity processing delays (in milliseconds)
|
||||
const ACTIVITY_DELAYS = {
|
||||
THROTTLE_MIN: 800,
|
||||
THROTTLE_MAX: 1400,
|
||||
ACTIVITY_SPACING_MIN: 1200,
|
||||
ACTIVITY_SPACING_MAX: 2600
|
||||
} as const
|
||||
|
||||
export class Workers {
|
||||
public bot: MicrosoftRewardsBot
|
||||
private jobState: JobState
|
||||
@@ -161,7 +170,7 @@ export class Workers {
|
||||
for (const activity of activities) {
|
||||
try {
|
||||
activityPage = await this.manageTabLifecycle(activityPage, activityInitial)
|
||||
await this.applyThrottle(throttle, 800, 1400)
|
||||
await this.applyThrottle(throttle, ACTIVITY_DELAYS.THROTTLE_MIN, ACTIVITY_DELAYS.THROTTLE_MAX)
|
||||
|
||||
const selector = await this.buildActivitySelector(activityPage, activity, punchCard)
|
||||
await this.prepareActivityPage(activityPage, selector, throttle)
|
||||
@@ -173,7 +182,7 @@ export class Workers {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
|
||||
}
|
||||
|
||||
await this.applyThrottle(throttle, 1200, 2600)
|
||||
await this.applyThrottle(throttle, ACTIVITY_DELAYS.ACTIVITY_SPACING_MIN, ACTIVITY_DELAYS.ACTIVITY_SPACING_MAX)
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
|
||||
throttle.record(false)
|
||||
@@ -209,7 +218,13 @@ export class Workers {
|
||||
|
||||
// Validate offerId exists before using it in selector
|
||||
if (!activity.offerId) {
|
||||
this.bot.log(this.bot.isMobile, 'WORKERS', `Activity "${activity.name || activity.title}" has no offerId, falling back to name-based selector`, 'warn')
|
||||
// IMPROVED: More prominent logging for data integrity issue
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'WORKERS',
|
||||
`⚠️ DATA INTEGRITY: Activity "${activity.name || activity.title}" is missing offerId field. This may indicate a dashboard API change or data corruption. Falling back to name-based selector.`,
|
||||
'warn'
|
||||
)
|
||||
return ACTIVITY_SELECTORS.byName(activity.name)
|
||||
}
|
||||
|
||||
@@ -217,9 +232,9 @@ export class Workers {
|
||||
}
|
||||
|
||||
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile))
|
||||
await page.waitForLoadState('networkidle', { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile))
|
||||
await this.bot.browser.utils.humanizePage(page)
|
||||
await this.applyThrottle(throttle, 1200, 2600)
|
||||
await this.applyThrottle(throttle, ACTIVITY_DELAYS.ACTIVITY_SPACING_MIN, ACTIVITY_DELAYS.ACTIVITY_SPACING_MAX)
|
||||
}
|
||||
|
||||
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
|
||||
@@ -227,14 +242,14 @@ export class Workers {
|
||||
|
||||
// Check if element exists before clicking (avoid 30s timeout)
|
||||
try {
|
||||
await page.waitForSelector(selector, { timeout: 5000 })
|
||||
await page.waitForSelector(selector, { timeout: TIMEOUTS.NETWORK_IDLE })
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity selector not found (might be completed or unavailable): ${selector}`, 'warn')
|
||||
return // Skip this activity gracefully instead of waiting 30s
|
||||
}
|
||||
|
||||
// Click with timeout to prevent indefinite hangs
|
||||
await page.click(selector, { timeout: 10000 })
|
||||
await page.click(selector, { timeout: TIMEOUTS.DASHBOARD_WAIT })
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
// Execute activity with timeout protection using Promise.race
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import type { MorePromotion, PromotionalItem } from './DashboardData'
|
||||
import type { Page } from 'playwright'
|
||||
import type { MorePromotion, PromotionalItem } from './DashboardData'
|
||||
|
||||
/**
|
||||
* Activity handler contract for solving a single dashboard activity.
|
||||
* Implementations should be stateless (or hold only a reference to the bot)
|
||||
* and perform all required steps on the provided page.
|
||||
*
|
||||
* **Extensibility Pattern**: This interface allows developers to register custom activity handlers
|
||||
* for new or unsupported activity types without modifying the core Activities.ts dispatcher.
|
||||
*
|
||||
* **Usage Example**:
|
||||
* ```typescript
|
||||
* class MyCustomHandler implements ActivityHandler {
|
||||
* id = 'my-custom-handler'
|
||||
* canHandle(activity) { return activity.name === 'special-promo' }
|
||||
* async run(page, activity) {
|
||||
* // Custom logic here
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // In bot initialization:
|
||||
* bot.activities.registerHandler(new MyCustomHandler())
|
||||
* ```
|
||||
*
|
||||
* **Notes**:
|
||||
* - Implementations should be stateless (or hold only a reference to the bot)
|
||||
* - The page is already navigated to the activity tab/window by the caller
|
||||
* - Custom handlers are checked BEFORE built-in handlers for maximum flexibility
|
||||
*/
|
||||
export interface ActivityHandler {
|
||||
/** Optional identifier used in logging output */
|
||||
|
||||
@@ -9,6 +9,24 @@ export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a standardized error message for logging
|
||||
* Ensures consistent error message formatting across all modules
|
||||
*
|
||||
* @param context - Context string (e.g., 'SEARCH-BING', 'LOGIN')
|
||||
* @param error - Error object or unknown value
|
||||
* @param prefix - Optional custom prefix (defaults to 'Error')
|
||||
* @returns Formatted error message
|
||||
*
|
||||
* @example
|
||||
* formatErrorMessage('SEARCH', err) // 'Error in SEARCH: Network timeout'
|
||||
* formatErrorMessage('LOGIN', err, 'Failed') // 'Failed in LOGIN: Invalid credentials'
|
||||
*/
|
||||
export function formatErrorMessage(context: string, error: unknown, prefix: string = 'Error'): string {
|
||||
const errorMsg = getErrorMessage(error)
|
||||
return `${prefix} in ${context}: ${errorMsg}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for common operations
|
||||
* IMPROVED: Added comprehensive documentation
|
||||
|
||||
Reference in New Issue
Block a user