/** * Desktop Flow Module * Extracted from index.ts to improve maintainability and testability * * Handles desktop browser automation: * - Login and session management * - Daily set completion * - More promotions * - Punch cards * - Desktop searches */ import type { MicrosoftRewardsBot } from '../index' import type { Account } from '../interface/Account' import { createBrowserInstance } from '../util/BrowserFactory' import { saveSessionData } from '../util/Load' export interface DesktopFlowResult { initialPoints: number collectedPoints: number } export class DesktopFlow { private bot: MicrosoftRewardsBot constructor(bot: MicrosoftRewardsBot) { this.bot = bot } /** * Execute the full desktop automation flow for an account * * 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 * ```typescript * const flow = new DesktopFlow(bot) * const result = await flow.run(account) * // result.collectedPoints contains points earned * ``` */ async run(account: Account): Promise { this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow') // IMPROVED: Use centralized browser factory to eliminate duplication const browser = await createBrowserInstance(this.bot, account.proxy, account.email) let keepBrowserOpen = false try { this.bot.homePage = await browser.newPage() this.bot.log(false, 'DESKTOP-FLOW', 'Browser started successfully') // Login into MS Rewards, then optionally stop if compromised await this.bot.login.login(this.bot.homePage, account.email, account.password, account.totp) if (this.bot.compromisedModeActive) { // User wants the page to remain open for manual recovery. Do not proceed to tasks. keepBrowserOpen = true const reason = this.bot.compromisedReason || 'security-issue' this.bot.log(false, 'DESKTOP-FLOW', `Account security check failed (${reason}). Browser kept open for manual review: ${account.email}`, 'warn', 'yellow') try { const { ConclusionWebhook } = await import('../util/ConclusionWebhook') await ConclusionWebhook( this.bot.config, '🔐 Security Check', `**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, activities paused`, undefined, 0xFFAA00 ) } 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 { await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, false) } catch (e) { this.bot.log(false, 'DESKTOP-FLOW', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') } return { initialPoints: 0, collectedPoints: 0 } } await this.bot.browser.func.goHome(this.bot.homePage) const data = await this.bot.browser.func.getDashboardData() const initial = data.userStatus.availablePoints this.bot.log(false, 'DESKTOP-FLOW', `Current point count: ${initial}`) const browserEarnablePoints = await this.bot.browser.func.getBrowserEarnablePoints() // Tally all the desktop points const pointsCanCollect = browserEarnablePoints.dailySetPoints + browserEarnablePoints.desktopSearchPoints + browserEarnablePoints.morePromotionsPoints this.bot.log(false, 'DESKTOP-FLOW', `You can earn ${pointsCanCollect} points today`) if (pointsCanCollect === 0) { // Extra diagnostic breakdown so users know WHY it's zero this.bot.log(false, 'DESKTOP-FLOW', `Breakdown (desktop): dailySet=${browserEarnablePoints.dailySetPoints} search=${browserEarnablePoints.desktopSearchPoints} promotions=${browserEarnablePoints.morePromotionsPoints}`) this.bot.log(false, 'DESKTOP-FLOW', 'All desktop earnable buckets are zero. This usually means: tasks already completed today OR the daily reset has not happened yet for your time zone. If you still want to force run activities set execution.runOnZeroPoints=true in config.', 'log', 'yellow') } // If runOnZeroPoints is false and 0 points to earn, don't continue if (!this.bot.config.runOnZeroPoints && pointsCanCollect === 0) { this.bot.log(false, 'DESKTOP-FLOW', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow') return { initialPoints: initial, collectedPoints: 0 } } // Open a new tab to where the tasks are going to be completed const workerPage = await browser.newPage() // Go to homepage on worker page await this.bot.browser.func.goHome(workerPage) // Complete daily set if (this.bot.config.workers.doDailySet) { await this.bot.workers.doDailySet(workerPage, data) } // Complete more promotions if (this.bot.config.workers.doMorePromotions) { await this.bot.workers.doMorePromotions(workerPage, data) } // Complete punch cards if (this.bot.config.workers.doPunchCards) { await this.bot.workers.doPunchCard(workerPage, data) } // Do desktop searches if (this.bot.config.workers.doDesktopSearch) { await this.bot.activities.doSearch(workerPage, data) } // Fetch points BEFORE closing (avoid page closed reload error) const after = await this.bot.browser.func.getCurrentPoints().catch(() => initial) return { initialPoints: initial, collectedPoints: (after - initial) || 0 } } finally { if (!keepBrowserOpen) { try { await this.bot.browser.func.closeBrowser(browser, account.email) } catch (closeError) { const message = closeError instanceof Error ? closeError.message : String(closeError) this.bot.log(false, 'DESKTOP-FLOW', `Failed to close desktop context: ${message}`, 'warn') } } } } }