diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index a8993eb..32ba8cc 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -11,6 +11,7 @@ export class AccountCreator { private dataGenerator: DataGenerator private referralUrl?: string private rl: readline.Interface + private rlClosed = false constructor(referralUrl?: string) { this.referralUrl = referralUrl @@ -19,6 +20,7 @@ export class AccountCreator { input: process.stdin, output: process.stdout }) + this.rlClosed = false } // Human-like delay helper @@ -111,7 +113,6 @@ export class AccountCreator { const maxWaitTime = 60000 // 60 seconds const startTime = Date.now() - const startUrl = this.page.url() try { // STEP 1: Wait for any "Creating account" messages to appear AND disappear @@ -313,21 +314,20 @@ export class AccountCreator { log(false, 'CREATOR', `✅ Account created successfully: ${confirmedEmail}`, 'log', 'green') - // Cleanup readline interface - this.rl.close() - return createdAccount } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error during account creation: ${msg}`, 'error') log(false, 'CREATOR', '⚠️ Browser left open for inspection. Press Ctrl+C to exit.', 'warn', 'yellow') - - // Keep browser open and wait indefinitely - await new Promise(() => {}) // Never resolves - keeps process alive - - this.rl.close() return null + } finally { + try { + if (!this.rlClosed) { + this.rl.close() + this.rlClosed = true + } + } catch {/* ignore */} } } @@ -335,11 +335,12 @@ export class AccountCreator { if (this.referralUrl) { log(false, 'CREATOR', `Navigating to referral URL: ${this.referralUrl}`, 'log', 'cyan') await this.page.goto(this.referralUrl, { waitUntil: 'networkidle', timeout: 60000 }) - await this.humanDelay(1500, 3000) + + await this.waitForPageStable('REFERRAL_PAGE', 20000) + await this.humanDelay(2000, 3000) log(false, 'CREATOR', 'Looking for "Join Microsoft Rewards" button...', 'log') - // Multiple selectors for the join button const joinButtonSelectors = [ 'a#start-earning-rewards-link', 'a.cta.learn-more-btn', @@ -354,7 +355,8 @@ export class AccountCreator { if (visible) { await button.click() - await this.humanDelay(2000, 4000) + await this.humanDelay(2000, 3000) + await this.waitForPageStable('AFTER_JOIN_CLICK', 15000) log(false, 'CREATOR', `✅ Clicked join button with selector: ${selector}`, 'log', 'green') clicked = true break @@ -368,14 +370,17 @@ export class AccountCreator { const url = 'https://login.live.com/' log(false, 'CREATOR', `No referral URL - navigating to: ${url}`, 'log', 'cyan') await this.page.goto(url, { waitUntil: 'networkidle', timeout: 60000 }) - await this.humanDelay(1500, 3000) + + await this.waitForPageStable('LOGIN_PAGE', 20000) + await this.humanDelay(2000, 3000) } } private async clickCreateAccount(): Promise { log(false, 'CREATOR', 'Looking for "Create account" button...', 'log') - // Multiple selectors for create account button + await this.waitForPageStable('BEFORE_CREATE_ACCOUNT', 15000) + const createAccountSelectors = [ 'a[id*="signup"]', 'a[href*="signup"]', @@ -390,7 +395,8 @@ export class AccountCreator { try { await button.waitFor({ timeout: 5000 }) await button.click() - await this.humanDelay(2000, 3500) + await this.humanDelay(2000, 3000) + await this.waitForPageStable('AFTER_CREATE_ACCOUNT', 15000) log(false, 'CREATOR', `✅ Clicked "Create account" with selector: ${selector}`, 'log', 'green') return @@ -405,6 +411,8 @@ export class AccountCreator { private async generateAndFillEmail(): Promise { log(false, 'CREATOR', '\n=== Email Configuration ===', 'log', 'cyan') + await this.waitForPageStable('EMAIL_PAGE', 15000) + const useAutoGenerate = await this.askQuestion('Generate email automatically? (Y/n): ') let email: string @@ -429,8 +437,8 @@ export class AccountCreator { await nextBtn.waitFor({ timeout: 15000 }) await nextBtn.click() await this.humanDelay(2000, 3000) + await this.waitForPageStable('AFTER_EMAIL_SUBMIT', 20000) - // Check for any error after clicking Next const result = await this.handleEmailErrors(email) if (!result.success) { return null @@ -440,7 +448,6 @@ export class AccountCreator { } private async handleEmailErrors(originalEmail: string): Promise<{ success: boolean; email: string | null }> { - // Wait for page to settle await this.humanDelay(1000, 1500) const errorLocator = this.page.locator('div[id*="Error"], div[role="alert"]').first() @@ -488,8 +495,8 @@ export class AccountCreator { const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) + await this.waitForPageStable('RETRY_EMAIL', 15000) - // Re-check for errors with the new email return await this.handleEmailErrors(newEmail) } @@ -497,6 +504,7 @@ export class AccountCreator { log(false, 'CREATOR', 'Email taken, looking for Microsoft suggestions...', 'log', 'yellow') await this.humanDelay(2000, 3000) + await this.waitForPageStable('EMAIL_SUGGESTIONS', 10000) // Multiple selectors for suggestions container const suggestionSelectors = [ @@ -668,18 +676,16 @@ export class AccountCreator { private async fillPassword(): Promise { log(false, 'CREATOR', 'Waiting for password page...', 'log') - // Wait for password title to appear (language-independent) await this.page.locator('h1[data-testid="title"]').first().waitFor({ timeout: 20000 }) + await this.waitForPageStable('PASSWORD_PAGE', 15000) await this.humanDelay(1000, 2000) log(false, 'CREATOR', 'Generating strong password...', 'log') const password = this.dataGenerator.generatePassword() - // Find password input const passwordInput = this.page.locator('input[type="password"]').first() await passwordInput.waitFor({ timeout: 15000 }) - // Clear and fill await passwordInput.clear() await this.humanDelay(500, 1000) await passwordInput.fill(password) @@ -727,11 +733,12 @@ export class AccountCreator { private async fillBirthdate(): Promise<{ day: number; month: number; year: number } | null> { log(false, 'CREATOR', 'Filling birthdate...', 'log') + await this.waitForPageStable('BIRTHDATE_PAGE', 15000) + const birthdate = this.dataGenerator.generateBirthdate() try { - // Fill day dropdown - wait for the page to be ready - await this.humanDelay(1000, 1500) + await this.humanDelay(2000, 3000) const dayButton = this.page.locator('button[name="BirthDay"], button#BirthDayDropdown').first() await dayButton.waitFor({ timeout: 15000, state: 'visible' }) @@ -842,10 +849,13 @@ export class AccountCreator { private async fillNames(email: string): Promise<{ firstName: string; lastName: string } | null> { log(false, 'CREATOR', 'Filling first and last name...', 'log') + await this.waitForPageStable('NAMES_PAGE', 15000) + const names = this.dataGenerator.generateNames(email) try { - // Fill first name with multiple selector fallbacks + await this.humanDelay(1000, 2000) + const firstNameSelectors = [ 'input[id*="firstName"]', 'input[name*="firstName"]', @@ -1044,6 +1054,7 @@ export class AccountCreator { if (yesVisible) { await yesButton.click() await this.humanDelay(2000, 3000) + await this.waitForPageStable('AFTER_KMSI', 15000) log(false, 'CREATOR', '✅ Accepted "Stay signed in"', 'log', 'green') found = true break @@ -1272,12 +1283,6 @@ export class AccountCreator { log(false, 'CREATOR', `Warning: Could not verify account: ${msg}`, 'warn', 'yellow') } } - - } catch (error) { - const msg = error instanceof Error ? error.message : String(error) - log(false, 'CREATOR', `Warning: Could not verify account: ${msg}`, 'warn', 'yellow') - } - } private async handleRewardsWelcomeTour(): Promise { log(false, 'CREATOR', 'Checking for Microsoft Rewards welcome tour...', 'log', 'cyan') @@ -1337,9 +1342,8 @@ export class AccountCreator { if (visible) { log(false, 'CREATOR', `Clicking Next button: ${selector}`, 'log', 'cyan') await button.click() - - // CRITICAL: Wait longer after clicking to let animation complete await this.humanDelay(3000, 4000) + await this.waitForPageStable('AFTER_TOUR_NEXT', 15000) clickedNext = true log(false, 'CREATOR', `✅ Clicked Next (step ${i + 1})`, 'log', 'green') @@ -1348,7 +1352,6 @@ export class AccountCreator { } if (!clickedNext) { - // Try "Pin and start earning" button (final step) const pinButtonSelectors = [ 'a#claim-button', 'a:has-text("Pin and start earning")', @@ -1364,6 +1367,7 @@ export class AccountCreator { log(false, 'CREATOR', 'Clicking "Pin and start earning" button', 'log', 'cyan') await button.click() await this.humanDelay(3000, 4000) + await this.waitForPageStable('AFTER_PIN', 15000) log(false, 'CREATOR', '✅ Clicked Pin button', 'log', 'green') break } @@ -1430,13 +1434,13 @@ export class AccountCreator { log(false, 'CREATOR', 'Clicking "Get started" button', 'log', 'cyan') await button.click() await this.humanDelay(3000, 4000) + await this.waitForPageStable('AFTER_GET_STARTED', 15000) log(false, 'CREATOR', '✅ Clicked Get started', 'log', 'green') break } } } - // Handle any other generic popups const genericCloseSelectors = [ 'button[aria-label*="Close"]', 'button[aria-label*="Fermer"]', @@ -1452,6 +1456,7 @@ export class AccountCreator { log(false, 'CREATOR', `Closing popup with selector: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(2000, 3000) + await this.waitForPageStable('AFTER_CLOSE_POPUP', 10000) } } @@ -1500,6 +1505,7 @@ export class AccountCreator { log(false, 'CREATOR', `Clicking "Join Microsoft Rewards" button: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(3000, 5000) + await this.waitForPageStable('AFTER_JOIN', 20000) log(false, 'CREATOR', '✅ Clicked Join button', 'log', 'green') joined = true break @@ -1607,7 +1613,10 @@ ${JSON.stringify(accountData, null, 2)}` } async close(): Promise { - this.rl.close() + if (!this.rlClosed) { + this.rl.close() + this.rlClosed = true + } if (this.page && !this.page.isClosed()) { await this.page.close() } diff --git a/src/index.ts b/src/index.ts index 5c7bb7d..c12a7f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -865,104 +865,109 @@ export class MicrosoftRewardsBot { async Desktop(account: Account) { log(false,'FLOW','Desktop() invoked') const browser = await this.browserFactory.createBrowser(account.proxy, account.email) - this.homePage = await browser.newPage() + let keepBrowserOpen = false + try { + this.homePage = await browser.newPage() - log(this.isMobile, 'MAIN', 'Starting browser') + log(this.isMobile, 'MAIN', 'Starting browser') - // Login into MS Rewards, then optionally stop if compromised - await this.login.login(this.homePage, account.email, account.password, account.totp) + // Login into MS Rewards, then optionally stop if compromised + await this.login.login(this.homePage, account.email, account.password, account.totp) - if (this.compromisedModeActive) { - // User wants the page to remain open for manual recovery. Do not proceed to tasks. - const reason = this.compromisedReason || 'security-issue' - log(this.isMobile, 'SECURITY', `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.config, - '🔐 Security Check', - `**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, activities paused`, - undefined, - 0xFFAA00 - ) - } catch {/* ignore */} - // Save session for convenience, but do not close the browser - try { - await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) - } catch (e) { - log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') + if (this.compromisedModeActive) { + // User wants the page to remain open for manual recovery. Do not proceed to tasks. + keepBrowserOpen = true + const reason = this.compromisedReason || 'security-issue' + log(this.isMobile, 'SECURITY', `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.config, + '🔐 Security Check', + `**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, activities paused`, + undefined, + 0xFFAA00 + ) + } catch {/* ignore */} + // Save session for convenience, but do not close the browser + try { + await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) + } catch (e) { + log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') + } + return { initialPoints: 0, collectedPoints: 0 } } - return { initialPoints: 0, collectedPoints: 0 } - } - await this.browser.func.goHome(this.homePage) + await this.browser.func.goHome(this.homePage) - const data = await this.browser.func.getDashboardData() + const data = await this.browser.func.getDashboardData() - const initial = data.userStatus.availablePoints + const initial = data.userStatus.availablePoints - log(this.isMobile, 'MAIN-POINTS', `Current point count: ${initial}`) + log(this.isMobile, 'MAIN-POINTS', `Current point count: ${initial}`) - const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints() + const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints() - // Tally all the desktop points - const pointsCanCollect = browserEnarablePoints.dailySetPoints + - browserEnarablePoints.desktopSearchPoints + - browserEnarablePoints.morePromotionsPoints + // Tally all the desktop points + const pointsCanCollect = browserEnarablePoints.dailySetPoints + + browserEnarablePoints.desktopSearchPoints + + browserEnarablePoints.morePromotionsPoints - log(this.isMobile, 'MAIN-POINTS', `You can earn ${pointsCanCollect} points today`) + log(this.isMobile, 'MAIN-POINTS', `You can earn ${pointsCanCollect} points today`) - if (pointsCanCollect === 0) { - // Extra diagnostic breakdown so users know WHY it's zero - log(this.isMobile, 'MAIN-POINTS', `Breakdown (desktop): dailySet=${browserEnarablePoints.dailySetPoints} search=${browserEnarablePoints.desktopSearchPoints} promotions=${browserEnarablePoints.morePromotionsPoints}`) - log(this.isMobile, 'MAIN-POINTS', '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 (pointsCanCollect === 0) { + // Extra diagnostic breakdown so users know WHY it's zero + log(this.isMobile, 'MAIN-POINTS', `Breakdown (desktop): dailySet=${browserEnarablePoints.dailySetPoints} search=${browserEnarablePoints.desktopSearchPoints} promotions=${browserEnarablePoints.morePromotionsPoints}`) + log(this.isMobile, 'MAIN-POINTS', '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.config.runOnZeroPoints && pointsCanCollect === 0) { - log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow') + // If runOnZeroPoints is false and 0 points to earn, don't continue + if (!this.config.runOnZeroPoints && pointsCanCollect === 0) { + log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow') + return { initialPoints: initial, collectedPoints: 0 } + } - // Close desktop browser - await this.browser.func.closeBrowser(browser, account.email) - return { initialPoints: initial, collectedPoints: 0 } - } + // Open a new tab to where the tasks are going to be completed + const workerPage = await browser.newPage() - // 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.browser.func.goHome(workerPage) - // Go to homepage on worker page - await this.browser.func.goHome(workerPage) + // Complete daily set + if (this.config.workers.doDailySet) { + await this.workers.doDailySet(workerPage, data) + } - // Complete daily set - if (this.config.workers.doDailySet) { - await this.workers.doDailySet(workerPage, data) - } + // Complete more promotions + if (this.config.workers.doMorePromotions) { + await this.workers.doMorePromotions(workerPage, data) + } - // Complete more promotions - if (this.config.workers.doMorePromotions) { - await this.workers.doMorePromotions(workerPage, data) - } + // Complete punch cards + if (this.config.workers.doPunchCards) { + await this.workers.doPunchCard(workerPage, data) + } - // Complete punch cards - if (this.config.workers.doPunchCards) { - await this.workers.doPunchCard(workerPage, data) - } + // Do desktop searches + if (this.config.workers.doDesktopSearch) { + await this.activities.doSearch(workerPage, data) + } - // Do desktop searches - if (this.config.workers.doDesktopSearch) { - await this.activities.doSearch(workerPage, data) - } - - // Save cookies - await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile) - - // Fetch points BEFORE closing (avoid page closed reload error) - const after = await this.browser.func.getCurrentPoints().catch(()=>initial) - // Close desktop browser - await this.browser.func.closeBrowser(browser, account.email) - return { - initialPoints: initial, - collectedPoints: (after - initial) || 0 + // Fetch points BEFORE closing (avoid page closed reload error) + const after = await this.browser.func.getCurrentPoints().catch(()=>initial) + return { + initialPoints: initial, + collectedPoints: (after - initial) || 0 + } + } finally { + if (!keepBrowserOpen) { + try { + await this.browser.func.closeBrowser(browser, account.email) + } catch (closeError) { + const message = closeError instanceof Error ? closeError.message : String(closeError) + this.log(this.isMobile, 'CLOSE-BROWSER', `Failed to close desktop context: ${message}`, 'warn') + } + } } } @@ -972,121 +977,138 @@ export class MicrosoftRewardsBot { ): Promise<{ initialPoints: number; collectedPoints: number }> { log(true,'FLOW','Mobile() invoked') const browser = await this.browserFactory.createBrowser(account.proxy, account.email) - this.homePage = await browser.newPage() + let keepBrowserOpen = false + let browserClosed = false + try { + this.homePage = await browser.newPage() - log(this.isMobile, 'MAIN', 'Starting browser') + log(this.isMobile, 'MAIN', 'Starting browser') - // Login into MS Rewards, then respect compromised mode - await this.login.login(this.homePage, account.email, account.password, account.totp) - if (this.compromisedModeActive) { - const reason = this.compromisedReason || 'security-issue' - log(this.isMobile, 'SECURITY', `Mobile security check failed (${reason}). Browser kept open for manual review: ${account.email}`, 'warn', 'yellow') - try { - const { ConclusionWebhook } = await import('./util/ConclusionWebhook') - await ConclusionWebhook( - this.config, - '🔐 Security Check (Mobile)', - `**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, mobile activities paused`, - undefined, - 0xFFAA00 - ) - } catch {/* ignore */} - try { - await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) - } catch (e) { - log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') + // Login into MS Rewards, then respect compromised mode + await this.login.login(this.homePage, account.email, account.password, account.totp) + if (this.compromisedModeActive) { + keepBrowserOpen = true + const reason = this.compromisedReason || 'security-issue' + log(this.isMobile, 'SECURITY', `Mobile security check failed (${reason}). Browser kept open for manual review: ${account.email}`, 'warn', 'yellow') + try { + const { ConclusionWebhook } = await import('./util/ConclusionWebhook') + await ConclusionWebhook( + this.config, + '🔐 Security Check (Mobile)', + `**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, mobile activities paused`, + undefined, + 0xFFAA00 + ) + } catch {/* ignore */} + try { + await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) + } catch (e) { + log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') + } + return { initialPoints: 0, collectedPoints: 0 } } - return { initialPoints: 0, collectedPoints: 0 } - } - const accessToken = await this.login.getMobileAccessToken(this.homePage, account.email, account.totp) - await this.browser.func.goHome(this.homePage) + const accessToken = await this.login.getMobileAccessToken(this.homePage, account.email, account.totp) + await this.browser.func.goHome(this.homePage) - const data = await this.browser.func.getDashboardData() - const initialPoints = data.userStatus.availablePoints || 0 + const data = await this.browser.func.getDashboardData() + const initialPoints = data.userStatus.availablePoints || 0 - const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints() - const appEarnablePoints = await this.browser.func.getAppEarnablePoints(accessToken) + const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints() + const appEarnablePoints = await this.browser.func.getAppEarnablePoints(accessToken) - const pointsCanCollect = browserEnarablePoints.mobileSearchPoints + appEarnablePoints.totalEarnablePoints + const pointsCanCollect = browserEnarablePoints.mobileSearchPoints + appEarnablePoints.totalEarnablePoints - log(this.isMobile, 'MAIN-POINTS', `You can earn ${pointsCanCollect} points today (Browser: ${browserEnarablePoints.mobileSearchPoints} points, App: ${appEarnablePoints.totalEarnablePoints} points)`) + log(this.isMobile, 'MAIN-POINTS', `You can earn ${pointsCanCollect} points today (Browser: ${browserEnarablePoints.mobileSearchPoints} points, App: ${appEarnablePoints.totalEarnablePoints} points)`) - if (pointsCanCollect === 0) { - log(this.isMobile, 'MAIN-POINTS', `Breakdown (mobile): browserSearch=${browserEnarablePoints.mobileSearchPoints} appTotal=${appEarnablePoints.totalEarnablePoints}`) - log(this.isMobile, 'MAIN-POINTS', 'All mobile earnable buckets are zero. Causes: mobile searches already maxed, daily set finished, or daily rollover not reached yet. You can force execution by setting execution.runOnZeroPoints=true.', 'log', 'yellow') - } + if (pointsCanCollect === 0) { + log(this.isMobile, 'MAIN-POINTS', `Breakdown (mobile): browserSearch=${browserEnarablePoints.mobileSearchPoints} appTotal=${appEarnablePoints.totalEarnablePoints}`) + log(this.isMobile, 'MAIN-POINTS', 'All mobile earnable buckets are zero. Causes: mobile searches already maxed, daily set finished, or daily rollover not reached yet. You can force execution by setting execution.runOnZeroPoints=true.', 'log', 'yellow') + } - // If runOnZeroPoints is false and 0 points to earn, don't continue - if (!this.config.runOnZeroPoints && pointsCanCollect === 0) { - log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow') + // If runOnZeroPoints is false and 0 points to earn, don't continue + if (!this.config.runOnZeroPoints && pointsCanCollect === 0) { + log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow') + + return { + initialPoints: initialPoints, + collectedPoints: 0 + } + } + // Do daily check in + if (this.config.workers.doDailyCheckIn) { + await this.activities.doDailyCheckIn(accessToken, data) + } + + // Do read to earn + if (this.config.workers.doReadToEarn) { + await this.activities.doReadToEarn(accessToken, data) + } + + // Do mobile searches + const configuredRetries = Number(this.config.searchSettings.retryMobileSearchAmount ?? 0) + const maxMobileRetries = Number.isFinite(configuredRetries) ? configuredRetries : 0 + + if (this.config.workers.doMobileSearch) { + // If no mobile searches data found, stop (Does not always exist on new accounts) + if (data.userStatus.counters.mobileSearch) { + // 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.browser.func.goHome(workerPage) + + await this.activities.doSearch(workerPage, data) + + // Fetch current search points + const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] + + if (mobileSearchPoints && (mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0) { + const shouldRetry = retryTracker.registerFailure() + + if (!shouldRetry) { + const exhaustedAttempts = retryTracker.getAttemptCount() + log(this.isMobile, 'MAIN', `Max retry limit of ${maxMobileRetries} reached after ${exhaustedAttempts} attempt(s). Exiting retry loop`, 'warn') + } else { + const attempt = retryTracker.getAttemptCount() + log(this.isMobile, 'MAIN', `Attempt ${attempt}/${maxMobileRetries}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow') + + // Close mobile browser before retrying to release resources + try { + await this.browser.func.closeBrowser(browser, account.email) + browserClosed = true + } catch (closeError) { + const message = closeError instanceof Error ? closeError.message : String(closeError) + this.log(this.isMobile, 'CLOSE-BROWSER', `Failed to close mobile context before retry: ${message}`, 'warn') + } + + // Create a new browser and try again with the same tracker + return await this.Mobile(account, retryTracker) + } + } + } else { + log(this.isMobile, 'MAIN', 'Unable to fetch search points, your account is most likely too "new" for this! Try again later!', 'warn') + } + } + + const afterPointAmount = await this.browser.func.getCurrentPoints() + + log(this.isMobile, 'MAIN-POINTS', `The script collected ${afterPointAmount - initialPoints} points today`) - // Close mobile browser - await this.browser.func.closeBrowser(browser, account.email) return { initialPoints: initialPoints, - collectedPoints: 0 + collectedPoints: (afterPointAmount - initialPoints) || 0 } - } - // Do daily check in - if (this.config.workers.doDailyCheckIn) { - await this.activities.doDailyCheckIn(accessToken, data) - } - - // Do read to earn - if (this.config.workers.doReadToEarn) { - await this.activities.doReadToEarn(accessToken, data) - } - - // Do mobile searches - const configuredRetries = Number(this.config.searchSettings.retryMobileSearchAmount ?? 0) - const maxMobileRetries = Number.isFinite(configuredRetries) ? configuredRetries : 0 - - if (this.config.workers.doMobileSearch) { - // If no mobile searches data found, stop (Does not always exist on new accounts) - if (data.userStatus.counters.mobileSearch) { - // 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.browser.func.goHome(workerPage) - - await this.activities.doSearch(workerPage, data) - - // Fetch current search points - const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] - - if (mobileSearchPoints && (mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0) { - const shouldRetry = retryTracker.registerFailure() - - if (!shouldRetry) { - const exhaustedAttempts = retryTracker.getAttemptCount() - log(this.isMobile, 'MAIN', `Max retry limit of ${maxMobileRetries} reached after ${exhaustedAttempts} attempt(s). Exiting retry loop`, 'warn') - } else { - const attempt = retryTracker.getAttemptCount() - log(this.isMobile, 'MAIN', `Attempt ${attempt}/${maxMobileRetries}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow') - - // Close mobile browser before retrying to release resources - await this.browser.func.closeBrowser(browser, account.email) - - // Create a new browser and try again with the same tracker - return await this.Mobile(account, retryTracker) - } + } finally { + if (!keepBrowserOpen && !browserClosed) { + try { + await this.browser.func.closeBrowser(browser, account.email) + browserClosed = true + } catch (closeError) { + const message = closeError instanceof Error ? closeError.message : String(closeError) + this.log(this.isMobile, 'CLOSE-BROWSER', `Failed to close mobile context: ${message}`, 'warn') } - } else { - log(this.isMobile, 'MAIN', 'Unable to fetch search points, your account is most likely too "new" for this! Try again later!', 'warn') } } - - const afterPointAmount = await this.browser.func.getCurrentPoints() - - log(this.isMobile, 'MAIN-POINTS', `The script collected ${afterPointAmount - initialPoints} points today`) - - // Close mobile browser - await this.browser.func.closeBrowser(browser, account.email) - return { - initialPoints: initialPoints, - collectedPoints: (afterPointAmount - initialPoints) || 0 - } } private async sendConclusion(summaries: AccountSummary[]) { diff --git a/src/util/Load.ts b/src/util/Load.ts index 1ab9cb9..1692914 100644 --- a/src/util/Load.ts +++ b/src/util/Load.ts @@ -4,7 +4,7 @@ import fs from 'fs' import path from 'path' import { Account } from '../interface/Account' -import { Config, ConfigSaveFingerprint, ConfigBrowser } from '../interface/Config' +import { Config, ConfigSaveFingerprint, ConfigBrowser, ConfigScheduling } from '../interface/Config' import { Util } from './Utils' const utils = new Util() @@ -209,6 +209,8 @@ function normalizeConfig(raw: unknown): Config { host: typeof dashboardRaw.host === 'string' ? dashboardRaw.host : '127.0.0.1' } + const scheduling = buildSchedulingConfig(n.scheduling) + const cfg: Config = { baseURL: n.baseURL ?? 'https://rewards.bing.com', sessionPath: n.sessionPath ?? 'sessions', @@ -239,12 +241,50 @@ function normalizeConfig(raw: unknown): Config { riskManagement, dryRun, queryDiversity, - dashboard + dashboard, + scheduling } return cfg } +function buildSchedulingConfig(raw: unknown): ConfigScheduling | undefined { + if (!raw || typeof raw !== 'object') return undefined + + const source = raw as Record + const scheduling: ConfigScheduling = { + enabled: source.enabled === true, + type: typeof source.type === 'string' ? source.type as ConfigScheduling['type'] : undefined + } + + const cronRaw = source.cron + if (cronRaw && typeof cronRaw === 'object') { + const cronSource = cronRaw as Record + scheduling.cron = { + schedule: typeof cronSource.schedule === 'string' ? cronSource.schedule : undefined, + workingDirectory: typeof cronSource.workingDirectory === 'string' ? cronSource.workingDirectory : undefined, + nodePath: typeof cronSource.nodePath === 'string' ? cronSource.nodePath : undefined, + logFile: typeof cronSource.logFile === 'string' ? cronSource.logFile : undefined, + user: typeof cronSource.user === 'string' ? cronSource.user : undefined + } + } + + const taskRaw = source.taskScheduler + if (taskRaw && typeof taskRaw === 'object') { + const taskSource = taskRaw as Record + scheduling.taskScheduler = { + taskName: typeof taskSource.taskName === 'string' ? taskSource.taskName : undefined, + schedule: typeof taskSource.schedule === 'string' ? taskSource.schedule : undefined, + frequency: typeof taskSource.frequency === 'string' ? taskSource.frequency as 'daily' | 'weekly' | 'once' : undefined, + workingDirectory: typeof taskSource.workingDirectory === 'string' ? taskSource.workingDirectory : undefined, + runAsUser: typeof taskSource.runAsUser === 'boolean' ? taskSource.runAsUser : undefined, + highestPrivileges: typeof taskSource.highestPrivileges === 'boolean' ? taskSource.highestPrivileges : undefined + } + } + + return scheduling +} + export function loadAccounts(): Account[] { try { // 1) CLI dev override