From 9fb5911fa20ddd6302e639119383d9deee0ad4d3 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sun, 9 Nov 2025 17:56:46 +0100 Subject: [PATCH] feat: Centralize browser instance creation to eliminate duplication in Desktop and Mobile flows fix: Update error message for HTTP 400 checks to English refactor: Improve logging for network idle wait timeout in Workers fix: Initialize lastError in Axios client to prevent undefined errors chore: Update tsconfig to include path mappings for better module resolution --- src/account-creation/cli.ts | 38 ++++++++++++++++++++++--------------- src/browser/BrowserFunc.ts | 13 +++++++++---- src/browser/BrowserUtil.ts | 2 +- src/flows/DesktopFlow.ts | 8 +++----- src/flows/MobileFlow.ts | 8 +++----- src/functions/Login.ts | 23 ++++++++++++++++------ src/functions/Workers.ts | 2 +- src/util/Axios.ts | 3 ++- src/util/BrowserFactory.ts | 33 ++++++++++++++++++++++++++++++++ tsconfig.json | 9 +++++++++ 10 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 src/util/BrowserFactory.ts diff --git a/src/account-creation/cli.ts b/src/account-creation/cli.ts index 3d824b9..af8cbd4 100644 --- a/src/account-creation/cli.ts +++ b/src/account-creation/cli.ts @@ -25,13 +25,15 @@ async function main(): Promise { } // Banner - console.log('\n' + '='.repeat(60)) + log(false, 'CREATOR-CLI', '', 'log') // Empty line + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan') log(false, 'CREATOR-CLI', '🚀 Microsoft Account Creator', 'log', 'cyan') - console.log('='.repeat(60)) - console.log('\x1b[33m⚠️ DO NOT INTERACT WITH THE BROWSER DURING AUTOMATION\x1b[0m') - console.log('\x1b[33m Everything is fully automated. Any interaction may break the process.\x1b[0m') - console.log('\x1b[33m Only interact when explicitly asked (e.g., CAPTCHA solving).\x1b[0m') - console.log('='.repeat(60) + '\n') + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan') + log(false, 'CREATOR-CLI', '⚠️ DO NOT INTERACT WITH THE BROWSER DURING AUTOMATION', 'warn', 'yellow') + log(false, 'CREATOR-CLI', ' Everything is fully automated. Any interaction may break the process.', 'warn', 'yellow') + log(false, 'CREATOR-CLI', ' Only interact when explicitly asked (e.g., CAPTCHA solving).', 'warn', 'yellow') + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan') + log(false, 'CREATOR-CLI', '', 'log') // Empty line // Display detected arguments if (referralUrl) { @@ -52,7 +54,7 @@ async function main(): Promise { log(false, 'CREATOR-CLI', '💡 Tip: Use -y flag to auto-accept all prompts', 'log', 'gray') } - console.log() + log(false, 'CREATOR-CLI', '', 'log') // Empty line // Create a temporary bot instance to access browser creation const bot = new MicrosoftRewardsBot(false) @@ -81,9 +83,10 @@ async function main(): Promise { if (result) { // Success banner - console.log('\n' + '='.repeat(60)) + log(false, 'CREATOR-CLI', '', 'log') // Empty line + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '✅ ACCOUNT CREATED SUCCESSFULLY!', 'log', 'green') - console.log('='.repeat(60)) + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') // Display account details log(false, 'CREATOR-CLI', `📧 Email: ${result.email}`, 'log', 'cyan') @@ -95,9 +98,10 @@ async function main(): Promise { log(false, 'CREATOR-CLI', '🔗 Referral: Linked', 'log', 'green') } - console.log('='.repeat(60)) + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '💾 Account details saved to accounts-created/ directory', 'log', 'green') - console.log('='.repeat(60) + '\n') + log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') + log(false, 'CREATOR-CLI', '', 'log') // Empty line // Keep browser open - don't close log(false, 'CREATOR-CLI', '✅ Account creation complete! Browser will remain open.', 'log', 'green') @@ -108,9 +112,11 @@ async function main(): Promise { await new Promise(() => {}) // Never resolves } else { // Failure - console.log('\n' + '='.repeat(60)) + log(false, 'CREATOR-CLI', '', 'log') // Empty line + log(false, 'CREATOR-CLI', '='.repeat(60), 'error') log(false, 'CREATOR-CLI', '❌ ACCOUNT CREATION FAILED', 'error') - console.log('='.repeat(60) + '\n') + log(false, 'CREATOR-CLI', '='.repeat(60), 'error') + log(false, 'CREATOR-CLI', '', 'log') // Empty line await browserContext.close() process.exit(1) @@ -118,9 +124,11 @@ async function main(): Promise { } catch (error) { const msg = error instanceof Error ? error.message : String(error) - console.log('\n' + '='.repeat(60)) + log(false, 'CREATOR-CLI', '', 'log') // Empty line + log(false, 'CREATOR-CLI', '='.repeat(60), 'error') log(false, 'CREATOR-CLI', `❌ Fatal error: ${msg}`, 'error') - console.log('='.repeat(60) + '\n') + log(false, 'CREATOR-CLI', '='.repeat(60), 'error') + log(false, 'CREATOR-CLI', '', 'log') // Empty line process.exit(1) } } diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index 9ff8165..6f4aed8 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -50,9 +50,10 @@ export default class BrowserFunc { this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error') return true } - } catch (e) { + } catch (error) { // Ignore errors in text check - not critical - this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${e}`, 'warn') + const errorMsg = error instanceof Error ? error.message : String(error) + this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn') } return false @@ -141,7 +142,8 @@ export default class BrowserFunc { await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((error) => { // Continuing is intentional: page may still be functional even if this specific element is missing // The script extraction will catch any real issues - this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${TIMEOUTS.DASHBOARD_WAIT}ms timeout, attempting to proceed: ${error instanceof Error ? error.message : String(error)}`, 'warn') + const errorMsg = error instanceof Error ? error.message : String(error) + this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${TIMEOUTS.DASHBOARD_WAIT}ms timeout, attempting to proceed: ${errorMsg}`, 'warn') }) let scriptContent = await this.extractDashboardScript(target) @@ -151,7 +153,10 @@ 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(logError('BROWSER-FUNC', 'Dashboard recovery load failed', this.bot.isMobile)) + await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch((error) => { + const errorMsg = error instanceof Error ? error.message : String(error) + this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load failed: ${errorMsg}`, 'warn') + }) await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM) scriptContent = await this.extractDashboardScript(target) diff --git a/src/browser/BrowserUtil.ts b/src/browser/BrowserUtil.ts index 9c9d0ba..ac097b1 100644 --- a/src/browser/BrowserUtil.ts +++ b/src/browser/BrowserUtil.ts @@ -201,7 +201,7 @@ export default class BrowserUtil { const isNetworkError = $('body.neterror').length const hasHttp400Error = html.includes('HTTP ERROR 400') || html.includes('This page isn\'t working') || - html.includes('Cette page ne fonctionne pas') + html.includes('This page is not working') if (isNetworkError || hasHttp400Error) { const errorType = hasHttp400Error ? 'HTTP 400' : 'network error' diff --git a/src/flows/DesktopFlow.ts b/src/flows/DesktopFlow.ts index c0f25e0..2eee53e 100644 --- a/src/flows/DesktopFlow.ts +++ b/src/flows/DesktopFlow.ts @@ -12,6 +12,7 @@ import type { MicrosoftRewardsBot } from '../index' import type { Account } from '../interface/Account' +import { createBrowserInstance } from '../util/BrowserFactory' import { saveSessionData } from '../util/Load' export interface DesktopFlowResult { @@ -34,11 +35,8 @@ export class DesktopFlow { async run(account: Account): Promise { this.bot.log(false, 'DESKTOP-FLOW', 'Starting desktop automation flow') - // FIXED: Use proper typed access instead of unsafe type assertion - const browserModule = await import('../browser/Browser') - const Browser = browserModule.default - const browserInstance = new Browser(this.bot) - const browser = await browserInstance.createBrowser(account.proxy, account.email) + // IMPROVED: Use centralized browser factory to eliminate duplication + const browser = await createBrowserInstance(this.bot, account.proxy, account.email) let keepBrowserOpen = false diff --git a/src/flows/MobileFlow.ts b/src/flows/MobileFlow.ts index 9d91941..9442cd1 100644 --- a/src/flows/MobileFlow.ts +++ b/src/flows/MobileFlow.ts @@ -13,6 +13,7 @@ import type { MicrosoftRewardsBot } from '../index' import type { Account } from '../interface/Account' +import { createBrowserInstance } from '../util/BrowserFactory' import { saveSessionData } from '../util/Load' import { MobileRetryTracker } from '../util/MobileRetryTracker' @@ -40,11 +41,8 @@ export class MobileFlow { ): Promise { this.bot.log(true, 'MOBILE-FLOW', 'Starting mobile automation flow') - // FIXED: Use proper typed access instead of unsafe type assertion - const browserModule = await import('../browser/Browser') - const Browser = browserModule.default - const browserInstance = new Browser(this.bot) - const browser = await browserInstance.createBrowser(account.proxy, account.email) + // IMPROVED: Use centralized browser factory to eliminate duplication + const browser = await createBrowserInstance(this.bot, account.proxy, account.email) let keepBrowserOpen = false let browserClosed = false diff --git a/src/functions/Login.ts b/src/functions/Login.ts index e269a9f..20177a3 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -177,7 +177,7 @@ export class Login { const content = await page.content().catch(() => '') const hasHttp400 = content.includes('HTTP ERROR 400') || content.includes('This page isn\'t working') || - content.includes('Cette page ne fonctionne pas') + content.includes('This page is not working') if (hasHttp400) { this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 detected in content, reloading...', 'warn') @@ -188,12 +188,20 @@ export class Login { await this.disableFido(page) - const [, , portalCheck] = await Promise.allSettled([ + const [reloadResult, totpResult, portalCheck] = await Promise.allSettled([ this.bot.browser.utils.reloadBadPage(page), this.tryAutoTotp(page, 'initial landing'), page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 3000 }) ]) + // Log any failures for debugging (non-critical) + if (reloadResult.status === 'rejected') { + this.bot.log(this.bot.isMobile, 'LOGIN', `Reload check failed (non-critical): ${reloadResult.reason}`, 'warn') + } + if (totpResult.status === 'rejected') { + this.bot.log(this.bot.isMobile, 'LOGIN', `Auto-TOTP check failed (non-critical): ${totpResult.reason}`, 'warn') + } + await this.checkAccountLocked(page) const alreadyAuthenticated = portalCheck.status === 'fulfilled' @@ -293,10 +301,13 @@ export class Login { if (!recoveryUsed) { await this.bot.utils.wait(500) - const content = await page.content().catch(() => '') + const content = await page.content().catch((err) => { + this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Failed to get page content for HTTP 400 check: ${err}`, 'warn') + return '' + }) const hasHttp400 = content.includes('HTTP ERROR 400') || content.includes('This page isn\'t working') || - content.includes('Cette page ne fonctionne pas') + content.includes('This page is not working') if (hasHttp400) { this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'HTTP 400 detected, reloading...', 'warn') @@ -435,7 +446,7 @@ export class Login { const content = await page.content().catch(() => '') const hasHttp400 = content.includes('HTTP ERROR 400') || content.includes('This page isn\'t working') || - content.includes('Cette page ne fonctionne pas') + content.includes('This page is not working') if (hasHttp400) { this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 on session check, reloading...', 'warn') @@ -1289,7 +1300,7 @@ export class Login { const content = await page.content().catch(() => '') const hasHttp400 = content.includes('HTTP ERROR 400') || content.includes('This page isn\'t working') || - content.includes('Cette page ne fonctionne pas') + content.includes('This page is not working') if (hasHttp400) { this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'HTTP 400 detected during Bing verification, reloading...', 'warn') diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index 15b0091..2777825 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -101,7 +101,7 @@ export class Workers { await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL }) // Wait for new page to load, max 10 seconds, however try regardless in case of error - await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { }) + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(logError('PUNCH-CARD', 'Network idle wait timeout (non-critical)', this.bot.isMobile)) await this.solveActivities(page, activitiesUncompleted, punchCard) diff --git a/src/util/Axios.ts b/src/util/Axios.ts index 0613322..7a712a6 100644 --- a/src/util/Axios.ts +++ b/src/util/Axios.ts @@ -87,7 +87,8 @@ class AxiosClient { return bypassInstance.request(config) } - let lastError: unknown + // FIXED: Initialize lastError to prevent throwing undefined + let lastError: unknown = new Error('Request failed with unknown error') const maxAttempts = 2 for (let attempt = 1; attempt <= maxAttempts; attempt++) { diff --git a/src/util/BrowserFactory.ts b/src/util/BrowserFactory.ts new file mode 100644 index 0000000..10ab76c --- /dev/null +++ b/src/util/BrowserFactory.ts @@ -0,0 +1,33 @@ +/** + * Browser Factory Utility + * Eliminates code duplication between Desktop and Mobile flows + * + * Centralized browser instance creation logic + */ + +import type { BrowserContext } from 'rebrowser-playwright' +import type { MicrosoftRewardsBot } from '../index' +import type { AccountProxy } from '../interface/Account' + +/** + * Create a browser instance for the given account + * IMPROVEMENT: Extracted from DesktopFlow and MobileFlow to eliminate duplication + * + * @param bot Bot instance + * @param proxy Account proxy configuration + * @param email Account email for session naming + * @returns Browser context ready to use + * + * @example + * const browser = await createBrowserInstance(bot, account.proxy, account.email) + */ +export async function createBrowserInstance( + bot: MicrosoftRewardsBot, + proxy: AccountProxy, + email: string +): Promise { + const browserModule = await import('../browser/Browser') + const Browser = browserModule.default + const browserInstance = new Browser(bot) + return await browserInstance.createBrowser(proxy, email) +} diff --git a/tsconfig.json b/tsconfig.json index 3463be8..bb32bf2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,15 @@ /* Module Resolution Options */ "moduleResolution": "node", + "baseUrl": "./src", + "paths": { + "@constants": ["constants"], + "@interfaces/*": ["interface/*"], + "@utils/*": ["util/*"], + "@browser/*": ["browser/*"], + "@functions/*": ["functions/*"], + "@flows/*": ["flows/*"] + }, "types": ["node"], "esModuleInterop": true,