mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 09:16:16 +00:00
Merge pull request #12 from LightZirconite/main
Latest update, fixing many issues before V3
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.59.1",
|
"version": "2.60.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.59.1",
|
"version": "2.60.0",
|
||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.59.1",
|
"version": "2.60.0",
|
||||||
"description": "Automate Microsoft Rewards points collection",
|
"description": "Automate Microsoft Rewards points collection",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"prestart": "node -e \"const fs = require('fs'); if (!fs.existsSync('dist/index.js')) { console.log('⚠️ Compiled files not found, building...'); require('child_process').execSync('npm run build', {stdio: 'inherit'}); }\"",
|
"prestart": "node -e \"const fs = require('fs'); if (!fs.existsSync('dist/index.js')) { console.log('⚠️ Compiled files not found, building...'); require('child_process').execSync('npm run build', {stdio: 'inherit'}); }\"",
|
||||||
"start": "node --enable-source-maps ./dist/index.js",
|
"start": "node --enable-source-maps ./dist/index.js",
|
||||||
"dev": "ts-node ./src/index.ts -dev",
|
"dev": "ts-node ./src/index.ts -dev",
|
||||||
"test": "node --test --loader ts-node/esm tests/**/*.test.ts",
|
"test": "node --test --require ts-node/register tests/**/*.test.ts",
|
||||||
"creator": "ts-node ./src/account-creation/cli.ts",
|
"creator": "ts-node ./src/account-creation/cli.ts",
|
||||||
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
|
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
|
||||||
"dashboard:dev": "ts-node ./src/index.ts -dashboard",
|
"dashboard:dev": "ts-node ./src/index.ts -dashboard",
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export class HumanBehavior {
|
|||||||
*/
|
*/
|
||||||
async microGestures(context: string): Promise<void> {
|
async microGestures(context: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const gestureNotes: string[] = []
|
||||||
|
|
||||||
// 60% chance of mouse movement (humans move mouse A LOT)
|
// 60% chance of mouse movement (humans move mouse A LOT)
|
||||||
if (Math.random() < 0.6) {
|
if (Math.random() < 0.6) {
|
||||||
const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px
|
const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px
|
||||||
@@ -115,8 +117,7 @@ export class HumanBehavior {
|
|||||||
// Mouse move failed - page may be closed or unavailable
|
// Mouse move failed - page may be closed or unavailable
|
||||||
})
|
})
|
||||||
|
|
||||||
// VERBOSE logging disabled - too noisy
|
gestureNotes.push(`mouse→(${x},${y})`)
|
||||||
// log(false, 'CREATOR', `[${context}] 🖱️ Mouse moved to (${x}, ${y})`, 'log', 'gray')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 30% chance of scroll (humans scroll to read content)
|
// 30% chance of scroll (humans scroll to read content)
|
||||||
@@ -129,8 +130,11 @@ export class HumanBehavior {
|
|||||||
// Scroll failed - page may be closed or unavailable
|
// Scroll failed - page may be closed or unavailable
|
||||||
})
|
})
|
||||||
|
|
||||||
// VERBOSE logging disabled - too noisy
|
gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`)
|
||||||
// log(false, 'CREATOR', `[${context}] 📜 Scrolled ${direction > 0 ? 'down' : 'up'} ${distance}px`, 'log', 'gray')
|
}
|
||||||
|
|
||||||
|
if (gestureNotes.length > 0) {
|
||||||
|
log(false, 'CREATOR', `[${context}] micro gestures: ${gestureNotes.join(', ')}`, 'log', 'gray')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Gesture execution failed - not critical for operation
|
// Gesture execution failed - not critical for operation
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export default class BrowserFunc {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG',
|
this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG',
|
||||||
`DOM Diagnostic - ` +
|
'DOM Diagnostic - ' +
|
||||||
`URL: ${diagnosticInfo.url}, ` +
|
`URL: ${diagnosticInfo.url}, ` +
|
||||||
`Title: "${diagnosticInfo.pageTitle}", ` +
|
`Title: "${diagnosticInfo.pageTitle}", ` +
|
||||||
`Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` +
|
`Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` +
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ export interface AccountResult {
|
|||||||
email: string
|
email: string
|
||||||
pointsEarned: number
|
pointsEarned: number
|
||||||
runDuration: number
|
runDuration: number
|
||||||
initialPoints: number // Points avant l'exécution
|
initialPoints: number // Points before execution
|
||||||
finalPoints: number // Points après l'exécution
|
finalPoints: number // Points after execution
|
||||||
desktopPoints: number // Points gagnés sur Desktop
|
desktopPoints: number // Points earned on desktop
|
||||||
mobilePoints: number // Points gagnés sur Mobile
|
mobilePoints: number // Points earned on mobile
|
||||||
errors?: string[]
|
errors?: string[]
|
||||||
banned?: boolean
|
banned?: boolean
|
||||||
}
|
}
|
||||||
@@ -38,11 +38,13 @@ export interface SummaryData {
|
|||||||
|
|
||||||
export class SummaryReporter {
|
export class SummaryReporter {
|
||||||
private config: Config
|
private config: Config
|
||||||
private jobState: JobState
|
private jobState?: JobState
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.jobState = new JobState(config)
|
if (config.jobState?.enabled !== false) {
|
||||||
|
this.jobState = new JobState(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +87,7 @@ export class SummaryReporter {
|
|||||||
description += ` | **🚫 Banned:** ${bannedCount}`
|
description += ` | **🚫 Banned:** ${bannedCount}`
|
||||||
}
|
}
|
||||||
|
|
||||||
description += `\n\n**📊 Account Details**\n`
|
description += '\n\n**📊 Account Details**\n'
|
||||||
|
|
||||||
const accountsWithErrors: AccountResult[] = []
|
const accountsWithErrors: AccountResult[] = []
|
||||||
|
|
||||||
@@ -100,13 +102,13 @@ export class SummaryReporter {
|
|||||||
description += `• Duration: ${durationSec}s\n`
|
description += `• Duration: ${durationSec}s\n`
|
||||||
|
|
||||||
// Collect accounts with errors for separate webhook
|
// Collect accounts with errors for separate webhook
|
||||||
if ((account.errors?.length || account.banned) && account.email) {
|
if (this.hasAccountFailure(account)) {
|
||||||
accountsWithErrors.push(account)
|
accountsWithErrors.push(account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer summary
|
// Footer summary
|
||||||
description += `\n**🌐 Total Balance**\n`
|
description += '\n**🌐 Total Balance**\n'
|
||||||
description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})`
|
description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})`
|
||||||
|
|
||||||
const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00
|
const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00
|
||||||
@@ -145,7 +147,7 @@ export class SummaryReporter {
|
|||||||
|
|
||||||
// Error details
|
// Error details
|
||||||
if (account.banned) {
|
if (account.banned) {
|
||||||
errorDescription += `• Status: Account Banned/Suspended\n`
|
errorDescription += '• Status: Account Banned/Suspended\n'
|
||||||
if (account.errors?.length && account.errors[0]) {
|
if (account.errors?.length && account.errors[0]) {
|
||||||
errorDescription += `• Reason: ${account.errors[0]}\n`
|
errorDescription += `• Reason: ${account.errors[0]}\n`
|
||||||
}
|
}
|
||||||
@@ -153,14 +155,14 @@ export class SummaryReporter {
|
|||||||
errorDescription += `• Error: ${account.errors[0]}\n`
|
errorDescription += `• Error: ${account.errors[0]}\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
errorDescription += `\n`
|
errorDescription += '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
errorDescription += `**📋 Recommended Actions:**\n`
|
errorDescription += '**📋 Recommended Actions:**\n'
|
||||||
errorDescription += `• Check account status manually\n`
|
errorDescription += '• Check account status manually\n'
|
||||||
errorDescription += `• Review error messages above\n`
|
errorDescription += '• Review error messages above\n'
|
||||||
errorDescription += `• Verify credentials if login failed\n`
|
errorDescription += '• Verify credentials if login failed\n'
|
||||||
errorDescription += `• Consider proxy rotation if rate-limited`
|
errorDescription += '• Consider proxy rotation if rate-limited'
|
||||||
|
|
||||||
await ConclusionWebhook(
|
await ConclusionWebhook(
|
||||||
this.config,
|
this.config,
|
||||||
@@ -195,6 +197,10 @@ export class SummaryReporter {
|
|||||||
* Update job state with completion status
|
* Update job state with completion status
|
||||||
*/
|
*/
|
||||||
async updateJobState(summary: SummaryData): Promise<void> {
|
async updateJobState(summary: SummaryData): Promise<void> {
|
||||||
|
if (!this.jobState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const day = summary.endTime.toISOString().split('T')?.[0]
|
const day = summary.endTime.toISOString().split('T')?.[0]
|
||||||
if (!day) return
|
if (!day) return
|
||||||
@@ -205,7 +211,7 @@ export class SummaryReporter {
|
|||||||
day,
|
day,
|
||||||
{
|
{
|
||||||
totalCollected: account.pointsEarned,
|
totalCollected: account.pointsEarned,
|
||||||
banned: false,
|
banned: account.banned ?? false,
|
||||||
errors: account.errors?.length ?? 0
|
errors: account.errors?.length ?? 0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -237,13 +243,15 @@ export class SummaryReporter {
|
|||||||
log('main', 'SUMMARY', '─'.repeat(80))
|
log('main', 'SUMMARY', '─'.repeat(80))
|
||||||
|
|
||||||
for (const account of summary.accounts) {
|
for (const account of summary.accounts) {
|
||||||
const status = account.errors?.length ? '❌ FAILED' : '✅ SUCCESS'
|
const status = this.hasAccountFailure(account) ? (account.banned ? '🚫 BANNED' : '❌ FAILED') : '✅ SUCCESS'
|
||||||
const duration = Math.round(account.runDuration / 1000)
|
const duration = Math.round(account.runDuration / 1000)
|
||||||
|
|
||||||
log('main', 'SUMMARY', `${status} | ${account.email}`)
|
log('main', 'SUMMARY', `${status} | ${account.email}`)
|
||||||
log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`)
|
log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`)
|
||||||
|
|
||||||
if (account.errors?.length) {
|
if (account.banned) {
|
||||||
|
log('main', 'SUMMARY', ' Status: Account flagged as banned/suspended', 'error')
|
||||||
|
} else if (account.errors?.length) {
|
||||||
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
|
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,8 +275,8 @@ export class SummaryReporter {
|
|||||||
endTime: Date
|
endTime: Date
|
||||||
): SummaryData {
|
): SummaryData {
|
||||||
const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0)
|
const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0)
|
||||||
const successCount = accounts.filter(acc => !acc.errors?.length).length
|
const failureCount = accounts.filter(acc => this.hasAccountFailure(acc)).length
|
||||||
const failureCount = accounts.length - successCount
|
const successCount = accounts.length - failureCount
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
@@ -279,4 +287,8 @@ export class SummaryReporter {
|
|||||||
failureCount
|
failureCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasAccountFailure(account: AccountResult): boolean {
|
||||||
|
return Boolean(account.errors?.length) || account.banned === true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
226
src/index.ts
226
src/index.ts
@@ -205,12 +205,12 @@ export class MicrosoftRewardsBot {
|
|||||||
// Only cluster when there's more than 1 cluster demanded
|
// Only cluster when there's more than 1 cluster demanded
|
||||||
if (this.config.clusters > 1) {
|
if (this.config.clusters > 1) {
|
||||||
if (cluster.isPrimary) {
|
if (cluster.isPrimary) {
|
||||||
this.runMaster()
|
await this.runMaster()
|
||||||
} else if (cluster.worker) {
|
} else if (cluster.worker) {
|
||||||
await this.runWorker()
|
await this.runWorker()
|
||||||
} else {
|
} else {
|
||||||
// Neither primary nor worker - something's wrong with clustering
|
// Neither primary nor worker - something's wrong with clustering
|
||||||
log('main', 'MAIN', `ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.`, 'error')
|
log('main', 'MAIN', 'ERROR: Cluster mode failed - neither primary nor worker! Falling back to single-process mode.', 'error')
|
||||||
const passes = this.config.passesPerRun ?? 1
|
const passes = this.config.passesPerRun ?? 1
|
||||||
for (let pass = 1; pass <= passes; pass++) {
|
for (let pass = 1; pass <= passes; pass++) {
|
||||||
if (passes > 1) {
|
if (passes > 1) {
|
||||||
@@ -279,101 +279,108 @@ export class MicrosoftRewardsBot {
|
|||||||
return this.accountSummaries
|
return this.accountSummaries
|
||||||
}
|
}
|
||||||
|
|
||||||
private runMaster() {
|
private runMaster(): Promise<void> {
|
||||||
log('main', 'MAIN-PRIMARY', 'Primary process started')
|
return new Promise((resolve) => {
|
||||||
|
log('main', 'MAIN-PRIMARY', 'Primary process started')
|
||||||
|
|
||||||
const totalAccounts = this.accounts.length
|
const totalAccounts = this.accounts.length
|
||||||
|
|
||||||
// Validate accounts exist
|
// Validate accounts exist
|
||||||
if (totalAccounts === 0) {
|
if (totalAccounts === 0) {
|
||||||
log('main', 'MAIN-PRIMARY', 'No accounts found to process. Exiting.', 'warn')
|
log('main', 'MAIN-PRIMARY', 'No accounts found to process. Nothing to do.', 'warn')
|
||||||
process.exit(0)
|
resolve()
|
||||||
}
|
return
|
||||||
|
|
||||||
// If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers.
|
|
||||||
const workerCount = Math.min(this.config.clusters, totalAccounts)
|
|
||||||
const accountChunks = this.utils.chunkArray(this.accounts, workerCount)
|
|
||||||
// Reset activeWorkers to actual spawn count (constructor used raw clusters)
|
|
||||||
this.activeWorkers = workerCount
|
|
||||||
|
|
||||||
// Store worker-to-chunk mapping for crash recovery
|
|
||||||
const workerChunkMap = new Map<number, Account[]>()
|
|
||||||
|
|
||||||
for (let i = 0; i < workerCount; i++) {
|
|
||||||
const worker = cluster.fork()
|
|
||||||
const chunk = accountChunks[i] || []
|
|
||||||
|
|
||||||
// Validate chunk has accounts
|
|
||||||
if (chunk.length === 0) {
|
|
||||||
log('main', 'MAIN-PRIMARY', `Warning: Worker ${i} received empty account chunk`, 'warn')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store chunk mapping for crash recovery
|
// If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers.
|
||||||
if (worker.id) {
|
const workerCount = Math.min(this.config.clusters, totalAccounts)
|
||||||
workerChunkMap.set(worker.id, chunk)
|
const accountChunks = this.utils.chunkArray(this.accounts, workerCount)
|
||||||
|
// Reset activeWorkers to actual spawn count (constructor used raw clusters)
|
||||||
|
this.activeWorkers = workerCount
|
||||||
|
|
||||||
|
// Store worker-to-chunk mapping for crash recovery
|
||||||
|
const workerChunkMap = new Map<number, Account[]>()
|
||||||
|
|
||||||
|
let resolved = false
|
||||||
|
const finishRun = async () => {
|
||||||
|
if (resolved) return
|
||||||
|
resolved = true
|
||||||
|
try {
|
||||||
|
await this.sendConclusion(this.accountSummaries)
|
||||||
|
} catch (e) {
|
||||||
|
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
}
|
||||||
|
log('main', 'MAIN-WORKER', 'All workers destroyed. Run complete.', 'warn')
|
||||||
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXED: Proper type checking before calling send
|
for (let i = 0; i < workerCount; i++) {
|
||||||
if (worker.send && typeof worker.send === 'function') {
|
const worker = cluster.fork()
|
||||||
worker.send({ chunk })
|
const chunk = accountChunks[i] || []
|
||||||
} else {
|
|
||||||
log('main', 'MAIN-PRIMARY', `ERROR: Worker ${i} does not have a send function!`, 'error')
|
// Validate chunk has accounts
|
||||||
|
if (chunk.length === 0) {
|
||||||
|
log('main', 'MAIN-PRIMARY', `Warning: Worker ${i} received empty account chunk`, 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store chunk mapping for crash recovery
|
||||||
|
if (worker.id) {
|
||||||
|
workerChunkMap.set(worker.id, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Proper type checking before calling send
|
||||||
|
if (worker.send && typeof worker.send === 'function') {
|
||||||
|
worker.send({ chunk })
|
||||||
|
} else {
|
||||||
|
log('main', 'MAIN-PRIMARY', `ERROR: Worker ${i} does not have a send function!`, 'error')
|
||||||
|
}
|
||||||
|
worker.on('message', (msg: unknown) => {
|
||||||
|
// IMPROVED: Using type-safe interface and type guard
|
||||||
|
if (isWorkerMessage(msg)) {
|
||||||
|
this.accountSummaries.push(...msg.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
worker.on('message', (msg: unknown) => {
|
|
||||||
// IMPROVED: Using type-safe interface and type guard
|
cluster.on('exit', (worker: Worker, code: number) => {
|
||||||
if (isWorkerMessage(msg)) {
|
this.activeWorkers -= 1
|
||||||
this.accountSummaries.push(...msg.data)
|
|
||||||
|
log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn')
|
||||||
|
|
||||||
|
// Optional: restart crashed worker (basic heuristic) if crashRecovery allows
|
||||||
|
const cr = this.config.crashRecovery
|
||||||
|
if (cr?.restartFailedWorker && code !== 0 && worker.id) {
|
||||||
|
const attempts = (worker as { _restartAttempts?: number })._restartAttempts || 0
|
||||||
|
if (attempts < (cr.restartFailedWorkerAttempts ?? 1)) {
|
||||||
|
(worker as { _restartAttempts?: number })._restartAttempts = attempts + 1
|
||||||
|
log('main', 'CRASH-RECOVERY', `Respawning worker (attempt ${attempts + 1})`, 'warn')
|
||||||
|
|
||||||
|
const originalChunk = workerChunkMap.get(worker.id)
|
||||||
|
const newW = cluster.fork()
|
||||||
|
|
||||||
|
if (originalChunk && originalChunk.length > 0 && newW.id) {
|
||||||
|
(newW as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk: originalChunk })
|
||||||
|
workerChunkMap.set(newW.id, originalChunk)
|
||||||
|
workerChunkMap.delete(worker.id)
|
||||||
|
log('main', 'CRASH-RECOVERY', `Assigned ${originalChunk.length} account(s) to respawned worker`)
|
||||||
|
} else {
|
||||||
|
log('main', 'CRASH-RECOVERY', 'Warning: Could not reassign accounts to respawned worker', 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
|
newW.on('message', (msg: unknown) => {
|
||||||
|
// IMPROVED: Using type-safe interface and type guard
|
||||||
|
if (isWorkerMessage(msg)) {
|
||||||
|
this.accountSummaries.push(...msg.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all workers have exited
|
||||||
|
if (this.activeWorkers === 0) {
|
||||||
|
void finishRun()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
cluster.on('exit', (worker: Worker, code: number) => {
|
|
||||||
this.activeWorkers -= 1
|
|
||||||
|
|
||||||
log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn')
|
|
||||||
|
|
||||||
// Optional: restart crashed worker (basic heuristic) if crashRecovery allows
|
|
||||||
const cr = this.config.crashRecovery
|
|
||||||
if (cr?.restartFailedWorker && code !== 0 && worker.id) {
|
|
||||||
const attempts = (worker as { _restartAttempts?: number })._restartAttempts || 0
|
|
||||||
if (attempts < (cr.restartFailedWorkerAttempts ?? 1)) {
|
|
||||||
(worker as { _restartAttempts?: number })._restartAttempts = attempts + 1
|
|
||||||
log('main', 'CRASH-RECOVERY', `Respawning worker (attempt ${attempts + 1})`, 'warn')
|
|
||||||
|
|
||||||
const originalChunk = workerChunkMap.get(worker.id)
|
|
||||||
const newW = cluster.fork()
|
|
||||||
|
|
||||||
if (originalChunk && originalChunk.length > 0 && newW.id) {
|
|
||||||
(newW as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk: originalChunk })
|
|
||||||
workerChunkMap.set(newW.id, originalChunk)
|
|
||||||
workerChunkMap.delete(worker.id)
|
|
||||||
log('main', 'CRASH-RECOVERY', `Assigned ${originalChunk.length} account(s) to respawned worker`)
|
|
||||||
} else {
|
|
||||||
log('main', 'CRASH-RECOVERY', 'Warning: Could not reassign accounts to respawned worker', 'warn')
|
|
||||||
}
|
|
||||||
|
|
||||||
newW.on('message', (msg: unknown) => {
|
|
||||||
// IMPROVED: Using type-safe interface and type guard
|
|
||||||
if (isWorkerMessage(msg)) {
|
|
||||||
this.accountSummaries.push(...msg.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all workers have exited
|
|
||||||
if (this.activeWorkers === 0) {
|
|
||||||
// All workers done -> send conclusion and exit (update check moved to startup)
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await this.sendConclusion(this.accountSummaries)
|
|
||||||
} catch (e) {
|
|
||||||
log('main', 'CONCLUSION', `Failed to send conclusion: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
|
||||||
}
|
|
||||||
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
|
||||||
process.exit(0)
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,11 +389,21 @@ export class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
// Wait for chunk (either already received during init, or will arrive soon)
|
// Wait for chunk (either already received during init, or will arrive soon)
|
||||||
const chunk = await new Promise<Account[]>((resolve) => {
|
const chunk = await new Promise<Account[]>((resolve) => {
|
||||||
if ((global as any).__workerChunk) {
|
if (global.__workerChunk) {
|
||||||
resolve((global as any).__workerChunk)
|
const bufferedChunk = global.__workerChunk
|
||||||
} else {
|
global.__workerChunk = undefined
|
||||||
(process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk: c }: { chunk: Account[] }) => resolve(c))
|
resolve(bufferedChunk)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMessage = (message: unknown): void => {
|
||||||
|
if (isWorkerChunkMessage(message)) {
|
||||||
|
process.off('message', handleMessage)
|
||||||
|
resolve(message.chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('message', handleMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!chunk || chunk.length === 0) {
|
if (!chunk || chunk.length === 0) {
|
||||||
@@ -896,6 +913,20 @@ function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
|||||||
return m.type === 'summary' && Array.isArray(m.data)
|
return m.type === 'summary' && Array.isArray(m.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WorkerChunkMessage {
|
||||||
|
chunk: Account[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWorkerChunkMessage(message: unknown): message is WorkerChunkMessage {
|
||||||
|
if (!message || typeof message !== 'object') return false
|
||||||
|
return Array.isArray((message as WorkerChunkMessage).chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __workerChunk: Account[] | undefined
|
||||||
|
}
|
||||||
|
|
||||||
// Use utility functions from Utils.ts
|
// Use utility functions from Utils.ts
|
||||||
const shortErr = shortErrorMessage
|
const shortErr = shortErrorMessage
|
||||||
const formatFullError = formatDetailedError
|
const formatFullError = formatDetailedError
|
||||||
@@ -905,9 +936,14 @@ async function main(): Promise<void> {
|
|||||||
// Workers initialize for ~2 seconds before reaching runWorker(), so messages
|
// Workers initialize for ~2 seconds before reaching runWorker(), so messages
|
||||||
// sent by primary during initialization would be lost without this early listener
|
// sent by primary during initialization would be lost without this early listener
|
||||||
if (!cluster.isPrimary && cluster.worker) {
|
if (!cluster.isPrimary && cluster.worker) {
|
||||||
(process as unknown as { on: (ev: 'message', cb: (m: { chunk: Account[] }) => void) => void }).on('message', ({ chunk }: { chunk: Account[] }) => {
|
const bufferChunk = (message: unknown): void => {
|
||||||
(global as any).__workerChunk = chunk
|
if (isWorkerChunkMessage(message)) {
|
||||||
})
|
global.__workerChunk = message.chunk
|
||||||
|
process.off('message', bufferChunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('message', bufferChunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for dashboard mode flag (standalone dashboard)
|
// Check for dashboard mode flag (standalone dashboard)
|
||||||
|
|||||||
@@ -25,9 +25,16 @@ export function obfuscateWebhookUrl(url: string): string {
|
|||||||
return Buffer.from(url).toString('base64')
|
return Buffer.from(url).toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
|
||||||
|
|
||||||
export function deobfuscateWebhookUrl(encoded: string): string {
|
export function deobfuscateWebhookUrl(encoded: string): string {
|
||||||
|
const trimmed = encoded.trim()
|
||||||
|
if (!trimmed || !BASE64_REGEX.test(trimmed)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Buffer.from(encoded, 'base64').toString('utf-8')
|
return Buffer.from(trimmed, 'base64').toString('utf-8')
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ test('MobileFlow has run method', async () => {
|
|||||||
|
|
||||||
// Mock bot instance
|
// Mock bot instance
|
||||||
const mockBot = {
|
const mockBot = {
|
||||||
log: () => {},
|
log: () => { },
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
config: {
|
config: {
|
||||||
workers: {},
|
workers: {},
|
||||||
@@ -52,10 +52,10 @@ test('MobileFlowResult interface has correct structure', async () => {
|
|||||||
|
|
||||||
test('MobileFlow accepts retry tracker', async () => {
|
test('MobileFlow accepts retry tracker', async () => {
|
||||||
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
||||||
const { MobileRetryTracker } = await import('../../src/util/MobileRetryTracker')
|
const { MobileRetryTracker } = await import('../../src/util/state/MobileRetryTracker')
|
||||||
|
|
||||||
const mockBot = {
|
const mockBot = {
|
||||||
log: () => {},
|
log: () => { },
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
config: {
|
config: {
|
||||||
workers: {},
|
workers: {},
|
||||||
|
|||||||
@@ -39,8 +39,25 @@ test('SummaryReporter creates summary correctly', async () => {
|
|||||||
const reporter = new SummaryReporter(mockConfig as never)
|
const reporter = new SummaryReporter(mockConfig as never)
|
||||||
|
|
||||||
const accounts = [
|
const accounts = [
|
||||||
{ email: 'test@example.com', pointsEarned: 100, runDuration: 60000 },
|
{
|
||||||
{ email: 'test2@example.com', pointsEarned: 150, runDuration: 70000, errors: ['test error'] }
|
email: 'test@example.com',
|
||||||
|
pointsEarned: 100,
|
||||||
|
runDuration: 60000,
|
||||||
|
initialPoints: 1000,
|
||||||
|
finalPoints: 1100,
|
||||||
|
desktopPoints: 60,
|
||||||
|
mobilePoints: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'test2@example.com',
|
||||||
|
pointsEarned: 150,
|
||||||
|
runDuration: 70000,
|
||||||
|
initialPoints: 2000,
|
||||||
|
finalPoints: 2150,
|
||||||
|
desktopPoints: 90,
|
||||||
|
mobilePoints: 60,
|
||||||
|
errors: ['test error']
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const startTime = new Date('2025-01-01T10:00:00Z')
|
const startTime = new Date('2025-01-01T10:00:00Z')
|
||||||
@@ -67,7 +84,15 @@ test('SummaryData structure is correct', async () => {
|
|||||||
const reporter = new SummaryReporter(mockConfig as never)
|
const reporter = new SummaryReporter(mockConfig as never)
|
||||||
|
|
||||||
const summary = reporter.createSummary(
|
const summary = reporter.createSummary(
|
||||||
[{ email: 'test@example.com', pointsEarned: 50, runDuration: 30000 }],
|
[{
|
||||||
|
email: 'test@example.com',
|
||||||
|
pointsEarned: 50,
|
||||||
|
runDuration: 30000,
|
||||||
|
initialPoints: 500,
|
||||||
|
finalPoints: 550,
|
||||||
|
desktopPoints: 30,
|
||||||
|
mobilePoints: 20
|
||||||
|
}],
|
||||||
new Date(),
|
new Date(),
|
||||||
new Date()
|
new Date()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
|
import { LoginState, LoginStateDetector } from '../src/util/validation/LoginStateDetector'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for LoginStateDetector - login flow state machine
|
* Tests for LoginStateDetector - login flow state machine
|
||||||
@@ -10,6 +10,8 @@ import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
|
|||||||
// Type helper for mock Page objects in tests
|
// Type helper for mock Page objects in tests
|
||||||
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
|
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
|
||||||
|
|
||||||
|
const asMockPage = <T>(page: T): MockPage => page as unknown as MockPage
|
||||||
|
|
||||||
test('LoginState enum contains expected states', () => {
|
test('LoginState enum contains expected states', () => {
|
||||||
assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
|
assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
|
||||||
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
|
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
|
||||||
@@ -60,7 +62,7 @@ test('detectState identifies LoggedIn state on rewards domain', async () => {
|
|||||||
evaluate: () => Promise.resolve(200)
|
evaluate: () => Promise.resolve(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
|
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -88,7 +90,7 @@ test('detectState identifies EmailPage state on login.live.com', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
|
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -115,7 +117,7 @@ test('detectState identifies PasswordPage state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
|
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -142,7 +144,7 @@ test('detectState identifies TwoFactorRequired state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
|
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -170,7 +172,7 @@ test('detectState identifies PasskeyPrompt state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
|
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -198,7 +200,7 @@ test('detectState identifies Blocked state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
|
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -216,7 +218,7 @@ test('detectState returns Unknown for ambiguous pages', async () => {
|
|||||||
evaluate: () => Promise.resolve(50)
|
evaluate: () => Promise.resolve(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
|
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
|
||||||
assert.equal(detection.confidence, 'low', 'Should have low confidence')
|
assert.equal(detection.confidence, 'low', 'Should have low confidence')
|
||||||
@@ -234,7 +236,7 @@ test('detectState handles errors gracefully', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await LoginStateDetector.detectState(mockPage as MockPage)
|
await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||||
assert.fail('Should throw error')
|
assert.fail('Should throw error')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assert.ok(e instanceof Error, 'Should throw Error instance')
|
assert.ok(e instanceof Error, 'Should throw Error instance')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
import { MobileRetryTracker } from '../src/util/MobileRetryTracker'
|
import { MobileRetryTracker } from '../src/util/state/MobileRetryTracker'
|
||||||
|
|
||||||
test('MobileRetryTracker stops retries after configured limit', () => {
|
test('MobileRetryTracker stops retries after configured limit', () => {
|
||||||
const tracker = new MobileRetryTracker(2)
|
const tracker = new MobileRetryTracker(2)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
import { QueryDiversityEngine } from '../src/util/QueryDiversityEngine'
|
import { QueryDiversityEngine } from '../src/util/network/QueryDiversityEngine'
|
||||||
|
|
||||||
test('QueryDiversityEngine fetches and limits queries', async () => {
|
test('QueryDiversityEngine fetches and limits queries', async () => {
|
||||||
const engine = new QueryDiversityEngine({
|
const engine = new QueryDiversityEngine({
|
||||||
@@ -13,7 +13,7 @@ test('QueryDiversityEngine fetches and limits queries', async () => {
|
|||||||
|
|
||||||
assert.ok(queries.length > 0, 'Should return at least one query')
|
assert.ok(queries.length > 0, 'Should return at least one query')
|
||||||
assert.ok(queries.length <= 10, 'Should respect count limit')
|
assert.ok(queries.length <= 10, 'Should respect count limit')
|
||||||
assert.ok(queries.every(q => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings')
|
assert.ok(queries.every((q: string) => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('QueryDiversityEngine deduplicates queries', async () => {
|
test('QueryDiversityEngine deduplicates queries', async () => {
|
||||||
|
|||||||
@@ -3,18 +3,8 @@
|
|||||||
* Tests intelligent page readiness and element detection
|
* Tests intelligent page readiness and element detection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert'
|
import assert from 'node:assert';
|
||||||
import { describe, it } from 'node:test'
|
import { describe, it } from 'node:test';
|
||||||
|
|
||||||
// Mock Playwright types for testing
|
|
||||||
type MockPage = {
|
|
||||||
url: () => string
|
|
||||||
content: () => Promise<string>
|
|
||||||
waitForLoadState: (state: string, options?: { timeout: number }) => Promise<void>
|
|
||||||
waitForTimeout: (ms: number) => Promise<void>
|
|
||||||
locator: (selector: string) => MockLocator
|
|
||||||
evaluate: <T>(fn: () => T) => Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockLocator = {
|
type MockLocator = {
|
||||||
waitFor: (options: { state: string; timeout: number }) => Promise<void>
|
waitFor: (options: { state: string; timeout: number }) => Promise<void>
|
||||||
@@ -94,7 +84,7 @@ describe('SmartWait', () => {
|
|||||||
mockLogFn('✓ Element found quickly (567ms)')
|
mockLogFn('✓ Element found quickly (567ms)')
|
||||||
|
|
||||||
assert.strictEqual(logs.length, 2, 'Should capture log messages')
|
assert.strictEqual(logs.length, 2, 'Should capture log messages')
|
||||||
assert.ok(logs[0].includes('1234ms'), 'Should include timing data')
|
assert.ok(logs[0]?.includes('1234ms'), 'Should include timing data')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should extract performance metrics from logs', () => {
|
it('should extract performance metrics from logs', () => {
|
||||||
@@ -102,8 +92,8 @@ describe('SmartWait', () => {
|
|||||||
const timeMatch = logMessage.match(/(\d+)ms/)
|
const timeMatch = logMessage.match(/(\d+)ms/)
|
||||||
|
|
||||||
assert.ok(timeMatch, 'Should include parseable timing')
|
assert.ok(timeMatch, 'Should include parseable timing')
|
||||||
if (timeMatch) {
|
if (timeMatch && timeMatch[1]) {
|
||||||
const time = parseInt(timeMatch[1])
|
const time = parseInt(timeMatch[1], 10)
|
||||||
assert.ok(time > 0, 'Should extract valid timing')
|
assert.ok(time > 0, 'Should extract valid timing')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user