import { AsyncLocalStorage } from 'node:async_hooks' import cluster, { Worker } from 'cluster' import type { BrowserContext, Cookie, Page } from 'patchright' import pkg from '../package.json' import type { BrowserFingerprintWithHeaders } from 'fingerprint-generator' import Browser from './browser/Browser' import BrowserFunc from './browser/BrowserFunc' import BrowserUtils from './browser/BrowserUtils' import { IpcLog, Logger } from './logging/Logger' import Utils from './util/Utils' import { loadAccounts, loadConfig } from './util/Load' import { checkNodeVersion } from './util/Validator' import { Login } from './browser/auth/Login' import { Workers } from './functions/Workers' import Activities from './functions/Activities' import { SearchManager } from './functions/SearchManager' import type { Account } from './interface/Account' import AxiosClient from './util/Axios' import { sendDiscord, flushDiscordQueue } from './logging/Discord' import { sendNtfy, flushNtfyQueue } from './logging/Ntfy' import type { DashboardData } from './interface/DashboardData' import type { AppDashboardData } from './interface/AppDashBoardData' interface ExecutionContext { isMobile: boolean account: Account } interface BrowserSession { context: BrowserContext fingerprint: BrowserFingerprintWithHeaders } interface AccountStats { email: string initialPoints: number finalPoints: number collectedPoints: number duration: number success: boolean error?: string } const executionContext = new AsyncLocalStorage() export function getCurrentContext(): ExecutionContext { const context = executionContext.getStore() if (!context) { return { isMobile: false, account: {} as any } } return context } async function flushAllWebhooks(timeoutMs = 5000): Promise { await Promise.allSettled([flushDiscordQueue(timeoutMs), flushNtfyQueue(timeoutMs)]) } interface UserData { userName: string geoLocale: string langCode: string initialPoints: number currentPoints: number gainedPoints: number } export class MicrosoftRewardsBot { public logger: Logger public config public utils: Utils public activities: Activities = new Activities(this) public browser: { func: BrowserFunc; utils: BrowserUtils } public mainMobilePage!: Page public mainDesktopPage!: Page public userData: UserData public accessToken = '' public requestToken = '' public cookies: { mobile: Cookie[]; desktop: Cookie[] } public fingerprint!: BrowserFingerprintWithHeaders private pointsCanCollect = 0 private activeWorkers: number private exitedWorkers: number[] private browserFactory: Browser = new Browser(this) private accounts: Account[] private workers: Workers private login = new Login(this) private searchManager: SearchManager public axios!: AxiosClient constructor() { this.userData = { userName: '', geoLocale: 'US', langCode: 'en', initialPoints: 0, currentPoints: 0, gainedPoints: 0 } this.logger = new Logger(this) this.accounts = [] this.cookies = { mobile: [], desktop: [] } this.utils = new Utils() this.workers = new Workers(this) this.searchManager = new SearchManager(this) this.browser = { func: new BrowserFunc(this), utils: new BrowserUtils(this) } this.config = loadConfig() this.activeWorkers = this.config.clusters this.exitedWorkers = [] } get isMobile(): boolean { return getCurrentContext().isMobile } async initialize(): Promise { this.accounts = loadAccounts() } async run(): Promise { const totalAccounts = this.accounts.length const runStartTime = Date.now() this.logger.info( 'main', 'RUN-START', `Starting Microsoft Rewards Script | v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}` ) if (this.config.clusters > 1) { if (cluster.isPrimary) { this.runMaster(runStartTime) } else { this.runWorker(runStartTime) } } else { await this.runTasks(this.accounts, runStartTime) } } private runMaster(runStartTime: number): void { void this.logger.info('main', 'CLUSTER-PRIMARY', `Primary process started | PID: ${process.pid}`) const rawChunks = this.utils.chunkArray(this.accounts, this.config.clusters) const accountChunks = rawChunks.filter(c => c && c.length > 0) this.activeWorkers = accountChunks.length const allAccountStats: AccountStats[] = [] for (const chunk of accountChunks) { const worker = cluster.fork() worker.send?.({ chunk, runStartTime }) worker.on('message', (msg: { __ipcLog?: IpcLog; __stats?: AccountStats[] }) => { if (msg.__stats) { allAccountStats.push(...msg.__stats) } const log = msg.__ipcLog if (log && typeof log.content === 'string') { const config = this.config const webhook = config.webhook const content = log.content const level = log.level if (webhook.discord?.enabled && webhook.discord.url) { sendDiscord(webhook.discord.url, content, level) } if (webhook.ntfy?.enabled && webhook.ntfy.url) { sendNtfy(webhook.ntfy, content, level) } } }) } const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise => { const { pid } = worker.process this.activeWorkers -= 1 if (!pid || this.exitedWorkers.includes(pid)) { return } else { this.exitedWorkers.push(pid) } this.logger.warn( 'main', `CLUSTER-WORKER-${label.toUpperCase()}`, `Worker ${worker.process?.pid ?? '?'} ${label} | Code: ${code ?? 'n/a'} | Active workers: ${this.activeWorkers}` ) if (this.activeWorkers <= 0) { const totalCollectedPoints = allAccountStats.reduce((sum, s) => sum + s.collectedPoints, 0) const totalInitialPoints = allAccountStats.reduce((sum, s) => sum + s.initialPoints, 0) const totalFinalPoints = allAccountStats.reduce((sum, s) => sum + s.finalPoints, 0) const totalDurationMinutes = ((Date.now() - runStartTime) / 1000 / 60).toFixed(1) this.logger.info( 'main', 'RUN-END', `Completed all accounts | Accounts processed: ${allAccountStats.length} | Total points collected: +${totalCollectedPoints} | Old total: ${totalInitialPoints} → New total: ${totalFinalPoints} | Total runtime: ${totalDurationMinutes}min`, 'green' ) await flushAllWebhooks() process.exit(code ?? 0) } } cluster.on('exit', (worker, code) => { void onWorkerDone('exit', worker, code) }) cluster.on('disconnect', worker => { void onWorkerDone('disconnect', worker, undefined) }) } private runWorker(runStartTimeFromMaster?: number): void { void this.logger.info('main', 'CLUSTER-WORKER-START', `Worker spawned | PID: ${process.pid}`) process.on('message', async ({ chunk, runStartTime }: { chunk: Account[]; runStartTime: number }) => { void this.logger.info( 'main', 'CLUSTER-WORKER-TASK', `Worker ${process.pid} received ${chunk.length} accounts.` ) try { const stats = await this.runTasks(chunk, runStartTime ?? runStartTimeFromMaster ?? Date.now()) if (process.send) { process.send({ __stats: stats }) } process.disconnect() } catch (error) { this.logger.error( 'main', 'CLUSTER-WORKER-ERROR', `Worker task crash: ${error instanceof Error ? error.message : String(error)}` ) await flushAllWebhooks() process.exit(1) } }) } private async runTasks(accounts: Account[], runStartTime: number): Promise { const accountStats: AccountStats[] = [] for (const account of accounts) { const accountStartTime = Date.now() const accountEmail = account.email this.userData.userName = this.utils.getEmailUsername(accountEmail) try { this.logger.info( 'main', 'ACCOUNT-START', `Starting account: ${accountEmail} | geoLocale: ${account.geoLocale}` ) this.axios = new AxiosClient(account.proxy) const result: { initialPoints: number; collectedPoints: number } | undefined = await this.Main( account ).catch(error => { void this.logger.error( true, 'FLOW', `Mobile flow failed for ${accountEmail}: ${error instanceof Error ? error.message : String(error)}` ) return undefined }) const durationSeconds = ((Date.now() - accountStartTime) / 1000).toFixed(1) if (result) { const collectedPoints = result.collectedPoints ?? 0 const accountInitialPoints = result.initialPoints ?? 0 const accountFinalPoints = accountInitialPoints + collectedPoints accountStats.push({ email: accountEmail, initialPoints: accountInitialPoints, finalPoints: accountFinalPoints, collectedPoints: collectedPoints, duration: parseFloat(durationSeconds), success: true }) this.logger.info( 'main', 'ACCOUNT-END', `Completed account: ${accountEmail} | Total: +${collectedPoints} | Old: ${accountInitialPoints} → New: ${accountFinalPoints} | Duration: ${durationSeconds}s`, 'green' ) } else { accountStats.push({ email: accountEmail, initialPoints: 0, finalPoints: 0, collectedPoints: 0, duration: parseFloat(durationSeconds), success: false, error: 'Flow failed' }) } } catch (error) { const durationSeconds = ((Date.now() - accountStartTime) / 1000).toFixed(1) this.logger.error( 'main', 'ACCOUNT-ERROR', `${accountEmail}: ${error instanceof Error ? error.message : String(error)}` ) accountStats.push({ email: accountEmail, initialPoints: 0, finalPoints: 0, collectedPoints: 0, duration: parseFloat(durationSeconds), success: false, error: error instanceof Error ? error.message : String(error) }) } } if (this.config.clusters <= 1 && !cluster.isWorker) { const totalCollectedPoints = accountStats.reduce((sum, s) => sum + s.collectedPoints, 0) const totalInitialPoints = accountStats.reduce((sum, s) => sum + s.initialPoints, 0) const totalFinalPoints = accountStats.reduce((sum, s) => sum + s.finalPoints, 0) const totalDurationMinutes = ((Date.now() - runStartTime) / 1000 / 60).toFixed(1) this.logger.info( 'main', 'RUN-END', `Completed all accounts | Accounts processed: ${accountStats.length} | Total points collected: +${totalCollectedPoints} | Old total: ${totalInitialPoints} → New total: ${totalFinalPoints} | Total runtime: ${totalDurationMinutes}min`, 'green' ) await flushAllWebhooks() process.exit() } return accountStats } async Main(account: Account): Promise<{ initialPoints: number; collectedPoints: number }> { const accountEmail = account.email this.logger.info('main', 'FLOW', `Starting session for ${accountEmail}`) let mobileSession: BrowserSession | null = null let mobileContextClosed = false try { return await executionContext.run({ isMobile: true, account }, async () => { mobileSession = await this.browserFactory.createBrowser(account) const initialContext: BrowserContext = mobileSession.context this.mainMobilePage = await initialContext.newPage() this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`) await this.login.login(this.mainMobilePage, account) try { this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail) } catch (error) { this.logger.error( 'main', 'FLOW', `Failed to get mobile access token: ${error instanceof Error ? error.message : String(error)}` ) } this.cookies.mobile = await initialContext.cookies() this.fingerprint = mobileSession.fingerprint const data: DashboardData = await this.browser.func.getDashboardData() const appData: AppDashboardData = await this.browser.func.getAppDashboardData() // Set geo this.userData.geoLocale = account.geoLocale === 'auto' ? data.userProfile.attributes.country : account.geoLocale.toLowerCase() if (this.userData.geoLocale.length > 2) { this.logger.warn( 'main', 'GEO-LOCALE', `The provided geoLocale is longer than 2 (${this.userData.geoLocale} | auto=${account.geoLocale === 'auto'}), this is likely invalid and can cause errors!` ) } this.userData.initialPoints = data.userStatus.availablePoints this.userData.currentPoints = data.userStatus.availablePoints const initialPoints = this.userData.initialPoints ?? 0 const browserEarnable = await this.browser.func.getBrowserEarnablePoints() const appEarnable = await this.browser.func.getAppEarnablePoints() this.pointsCanCollect = browserEarnable.mobileSearchPoints + (appEarnable?.totalEarnablePoints ?? 0) this.logger.info( 'main', 'POINTS', `Earnable today | Mobile: ${this.pointsCanCollect} | Browser: ${ browserEarnable.mobileSearchPoints } | App: ${appEarnable?.totalEarnablePoints ?? 0} | ${accountEmail} | locale: ${this.userData.geoLocale}` ) if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData) if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage) if (this.config.workers.doSpecialPromotions) await this.workers.doSpecialPromotions(data) if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage) if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn() if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn() const searchPoints = await this.browser.func.getSearchPoints() const missingSearchPoints = this.browser.func.missingSearchPoints(searchPoints, true) this.cookies.mobile = await initialContext.cookies() const { mobilePoints, desktopPoints } = await this.searchManager.doSearches( data, missingSearchPoints, mobileSession, account, accountEmail ) mobileContextClosed = true this.userData.gainedPoints = mobilePoints + desktopPoints const finalPoints = await this.browser.func.getCurrentPoints() const collectedPoints = finalPoints - initialPoints this.logger.info( 'main', 'FLOW', `Collected: +${collectedPoints} | Mobile: +${mobilePoints} | Desktop: +${desktopPoints} | ${accountEmail}` ) return { initialPoints, collectedPoints: collectedPoints || 0 } }) } finally { if (mobileSession && !mobileContextClosed) { try { await executionContext.run({ isMobile: true, account }, async () => { await this.browser.func.closeBrowser(mobileSession!.context, accountEmail) }) } catch {} } } } } export { executionContext } async function main(): Promise { // Check before doing anything checkNodeVersion() const rewardsBot = new MicrosoftRewardsBot() process.on('beforeExit', () => { void flushAllWebhooks() }) process.on('SIGINT', async () => { rewardsBot.logger.warn('main', 'PROCESS', 'SIGINT received, flushing and exiting...') await flushAllWebhooks() process.exit(130) }) process.on('SIGTERM', async () => { rewardsBot.logger.warn('main', 'PROCESS', 'SIGTERM received, flushing and exiting...') await flushAllWebhooks() process.exit(143) }) process.on('uncaughtException', async error => { rewardsBot.logger.error('main', 'UNCAUGHT-EXCEPTION', error) await flushAllWebhooks() process.exit(1) }) process.on('unhandledRejection', async reason => { rewardsBot.logger.error('main', 'UNHANDLED-REJECTION', reason as Error) await flushAllWebhooks() process.exit(1) }) try { await rewardsBot.initialize() await rewardsBot.run() } catch (error) { rewardsBot.logger.error('main', 'MAIN-ERROR', error as Error) } } main().catch(async error => { const tmpBot = new MicrosoftRewardsBot() tmpBot.logger.error('main', 'MAIN-ERROR', error as Error) await flushAllWebhooks() process.exit(1) })