mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-19 14:33:58 +00:00
513 lines
19 KiB
TypeScript
513 lines
19 KiB
TypeScript
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<ExecutionContext>()
|
|
|
|
export function getCurrentContext(): ExecutionContext {
|
|
const context = executionContext.getStore()
|
|
if (!context) {
|
|
return { isMobile: false, account: {} as any }
|
|
}
|
|
return context
|
|
}
|
|
|
|
async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
|
|
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<void> {
|
|
this.accounts = loadAccounts()
|
|
}
|
|
|
|
async run(): Promise<void> {
|
|
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<void> => {
|
|
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<AccountStats[]> {
|
|
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<void> {
|
|
// 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)
|
|
})
|