mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 09:46:16 +00:00
refactor: simplify bot control logic and improve error handling in routes
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import { dashboardState } from './state'
|
import { dashboardState } from './state'
|
||||||
|
import type { MicrosoftRewardsBot } from '../index'
|
||||||
// We'll import and run the bot directly in the same process
|
|
||||||
let botRunning = false
|
|
||||||
let botPromise: Promise<void> | null = null
|
|
||||||
|
|
||||||
export class BotController {
|
export class BotController {
|
||||||
private isRunning: boolean = false
|
private botInstance: MicrosoftRewardsBot | null = null
|
||||||
|
private botPromise: Promise<void> | null = null
|
||||||
private startTime?: Date
|
private startTime?: Date
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Cleanup on exit
|
|
||||||
process.on('exit', () => this.stop())
|
process.on('exit', () => this.stop())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,30 +23,26 @@ export class BotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
public async start(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||||
if (this.isRunning || botRunning) {
|
if (this.botInstance) {
|
||||||
return { success: false, error: 'Bot is already running' }
|
return { success: false, error: 'Bot is already running' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.log('🚀 Starting bot...', 'log')
|
this.log('🚀 Starting bot...', 'log')
|
||||||
|
|
||||||
// Import the bot main logic
|
|
||||||
const { MicrosoftRewardsBot } = await import('../index')
|
const { MicrosoftRewardsBot } = await import('../index')
|
||||||
|
|
||||||
this.isRunning = true
|
this.botInstance = new MicrosoftRewardsBot(false)
|
||||||
botRunning = true
|
|
||||||
this.startTime = new Date()
|
this.startTime = new Date()
|
||||||
dashboardState.setRunning(true)
|
dashboardState.setRunning(true)
|
||||||
|
dashboardState.setBotInstance(this.botInstance)
|
||||||
|
|
||||||
// Run the bot in the same process using the exact same logic as npm start
|
this.botPromise = (async () => {
|
||||||
botPromise = (async () => {
|
|
||||||
try {
|
try {
|
||||||
const rewardsBot = new MicrosoftRewardsBot(false)
|
|
||||||
|
|
||||||
this.log('✓ Bot initialized, starting execution...', 'log')
|
this.log('✓ Bot initialized, starting execution...', 'log')
|
||||||
|
|
||||||
await rewardsBot.initialize()
|
await this.botInstance!.initialize()
|
||||||
await rewardsBot.run()
|
await this.botInstance!.run()
|
||||||
|
|
||||||
this.log('✓ Bot completed successfully', 'log')
|
this.log('✓ Bot completed successfully', 'log')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -61,8 +54,7 @@ export class BotController {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Don't await - let it run in background
|
this.botPromise.catch(error => {
|
||||||
botPromise.catch(error => {
|
|
||||||
this.log(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
this.log(`Bot execution failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -77,15 +69,12 @@ export class BotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public stop(): { success: boolean; error?: string } {
|
public stop(): { success: boolean; error?: string } {
|
||||||
if (!this.isRunning && !botRunning) {
|
if (!this.botInstance) {
|
||||||
return { success: false, error: 'Bot is not running' }
|
return { success: false, error: 'Bot is not running' }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.log('🛑 Stopping bot...', 'warn')
|
this.log('🛑 Stopping bot...', 'warn')
|
||||||
|
|
||||||
// For now, we can't gracefully stop a running bot in the same process
|
|
||||||
// This would require refactoring the bot to support cancellation
|
|
||||||
this.log('⚠ Note: Bot will complete current task before stopping', 'warn')
|
this.log('⚠ Note: Bot will complete current task before stopping', 'warn')
|
||||||
|
|
||||||
this.cleanup()
|
this.cleanup()
|
||||||
@@ -103,7 +92,6 @@ export class BotController {
|
|||||||
this.log('🔄 Restarting bot...', 'log')
|
this.log('🔄 Restarting bot...', 'log')
|
||||||
this.stop()
|
this.stop()
|
||||||
|
|
||||||
// Wait a bit before restarting
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const result = await this.start()
|
const result = await this.start()
|
||||||
@@ -119,7 +107,7 @@ export class BotController {
|
|||||||
startTime?: string
|
startTime?: string
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
running: this.isRunning || botRunning,
|
running: !!this.botInstance,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
uptime: this.startTime ? Date.now() - this.startTime.getTime() : undefined,
|
uptime: this.startTime ? Date.now() - this.startTime.getTime() : undefined,
|
||||||
startTime: this.startTime?.toISOString()
|
startTime: this.startTime?.toISOString()
|
||||||
@@ -127,13 +115,12 @@ export class BotController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
this.isRunning = false
|
this.botInstance = null
|
||||||
botRunning = false
|
this.botPromise = null
|
||||||
botPromise = null
|
|
||||||
this.startTime = undefined
|
this.startTime = undefined
|
||||||
dashboardState.setRunning(false)
|
dashboardState.setRunning(false)
|
||||||
|
dashboardState.setBotInstance(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
|
||||||
export const botController = new BotController()
|
export const botController = new BotController()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Router, Request, Response } from 'express'
|
import { Router, Request, Response } from 'express'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { spawn } from 'child_process'
|
|
||||||
import { dashboardState } from './state'
|
import { dashboardState } from './state'
|
||||||
import { loadAccounts, loadConfig, getConfigPath } from '../util/Load'
|
import { loadAccounts, loadConfig, getConfigPath } from '../util/Load'
|
||||||
import { botController } from './BotController'
|
import { botController } from './BotController'
|
||||||
@@ -209,7 +208,7 @@ apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> =
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// POST /api/sync/:email - Force sync single account
|
// POST /api/sync/:email - Force sync single account (deprecated - use full bot restart)
|
||||||
apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise<void> => {
|
apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { email } = req.params
|
const { email } = req.params
|
||||||
@@ -226,18 +225,10 @@ apiRouter.post('/sync/:email', async (req: Request, res: Response): Promise<void
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardState.updateAccount(email, { status: 'running', lastSync: new Date().toISOString() })
|
res.status(501).json({
|
||||||
|
error: 'Single account sync not implemented. Please use restart bot instead.',
|
||||||
// Spawn single account run
|
suggestion: 'Use /api/restart endpoint to run all accounts'
|
||||||
const child = spawn(process.execPath, [
|
})
|
||||||
path.join(process.cwd(), 'dist', 'index.js'),
|
|
||||||
'-account',
|
|
||||||
email
|
|
||||||
], { detached: true, stdio: 'ignore' })
|
|
||||||
|
|
||||||
if (child.unref) child.unref()
|
|
||||||
|
|
||||||
res.json({ success: true, pid: child.pid || undefined })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' })
|
||||||
}
|
}
|
||||||
@@ -318,7 +309,13 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
|
|||||||
function maskUrl(url: string): string {
|
function maskUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url)
|
const parsed = new URL(url)
|
||||||
return `${parsed.protocol}//${parsed.hostname.slice(0, 3)}***${parsed.pathname.slice(0, 5)}***`
|
const maskedHost = parsed.hostname.length > 6
|
||||||
|
? `${parsed.hostname.slice(0, 3)}***${parsed.hostname.slice(-3)}`
|
||||||
|
: '***'
|
||||||
|
const maskedPath = parsed.pathname.length > 5
|
||||||
|
? `${parsed.pathname.slice(0, 3)}***`
|
||||||
|
: '***'
|
||||||
|
return `${parsed.protocol}//${maskedHost}${maskedPath}`
|
||||||
} catch {
|
} catch {
|
||||||
return '***'
|
return '***'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,17 +108,14 @@ export class DashboardServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private interceptBotLogs(): void {
|
private interceptBotLogs(): void {
|
||||||
// Store reference to this.clients for closure
|
|
||||||
const clients = this.clients
|
|
||||||
|
|
||||||
// Intercept bot logs and forward to dashboard
|
|
||||||
const originalLog = botLog
|
const originalLog = botLog
|
||||||
;(global as Record<string, unknown>).botLog = function(
|
|
||||||
|
;(global as Record<string, unknown>).botLog = (
|
||||||
isMobile: boolean | 'main',
|
isMobile: boolean | 'main',
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
type: 'log' | 'warn' | 'error' = 'log'
|
type: 'log' | 'warn' | 'error' = 'log'
|
||||||
) {
|
) => {
|
||||||
const result = originalLog(isMobile, title, message, type)
|
const result = originalLog(isMobile, title, message, type)
|
||||||
|
|
||||||
const logEntry: DashboardLog = {
|
const logEntry: DashboardLog = {
|
||||||
@@ -130,18 +127,7 @@ export class DashboardServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dashboardState.addLog(logEntry)
|
dashboardState.addLog(logEntry)
|
||||||
|
this.broadcastUpdate('log', { log: logEntry })
|
||||||
// Broadcast to WebSocket clients
|
|
||||||
const payload = JSON.stringify({ type: 'log', log: logEntry })
|
|
||||||
for (const client of clients) {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
try {
|
|
||||||
client.send(payload)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Dashboard] Error sending to WebSocket client:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/index.ts
74
src/index.ts
@@ -48,16 +48,12 @@ export class MicrosoftRewardsBot {
|
|||||||
public queryEngine?: QueryDiversityEngine
|
public queryEngine?: QueryDiversityEngine
|
||||||
public compromisedModeActive: boolean = false
|
public compromisedModeActive: boolean = false
|
||||||
public compromisedReason?: string
|
public compromisedReason?: string
|
||||||
// Mutex-like flag to prevent parallel execution when config.parallel is accidentally misconfigured
|
|
||||||
private isDesktopRunning: boolean = false
|
|
||||||
private isMobileRunning: boolean = false
|
|
||||||
|
|
||||||
private activeWorkers: number
|
private activeWorkers: number
|
||||||
private browserFactory: Browser = new Browser(this)
|
private browserFactory: Browser = new Browser(this)
|
||||||
private accounts: Account[]
|
private accounts: Account[]
|
||||||
private workers: Workers
|
private workers: Workers
|
||||||
private login = new Login(this)
|
private login = new Login(this)
|
||||||
// Buy mode (manual spending) tracking
|
|
||||||
private buyMode: { enabled: boolean; email?: string } = { enabled: false }
|
private buyMode: { enabled: boolean; email?: string } = { enabled: false }
|
||||||
|
|
||||||
// Summary collection (per process)
|
// Summary collection (per process)
|
||||||
@@ -632,55 +628,45 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sequential execution with safety checks
|
// Sequential execution with safety checks
|
||||||
if (this.isDesktopRunning || this.isMobileRunning) {
|
this.isMobile = false
|
||||||
log('main', 'TASK', `Race condition detected: Desktop=${this.isDesktopRunning}, Mobile=${this.isMobileRunning}. Skipping to prevent conflicts.`, 'error')
|
const desktopResult = await this.Desktop(account).catch(e => {
|
||||||
errors.push('race-condition-detected')
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
} else {
|
this.recordRiskEvent('error', 6, `desktop:${msg}`)
|
||||||
this.isMobile = false
|
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
this.isDesktopRunning = true
|
const bd = detectBanReason(e)
|
||||||
const desktopResult = await this.Desktop(account).catch(e => {
|
if (bd.status) {
|
||||||
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
|
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
||||||
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
|
}
|
||||||
|
errors.push(formatFullError('desktop', e, verbose)); return null
|
||||||
|
})
|
||||||
|
if (desktopResult) {
|
||||||
|
desktopInitial = desktopResult.initialPoints
|
||||||
|
desktopCollected = desktopResult.collectedPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!banned.status && !this.compromisedModeActive) {
|
||||||
|
this.isMobile = true
|
||||||
|
const mobileResult = await this.Mobile(account).catch((e: unknown) => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
this.recordRiskEvent('error', 6, `desktop:${msg}`)
|
this.recordRiskEvent('error', 6, `mobile:${msg}`)
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullError('desktop', e, verbose)); return null
|
errors.push(formatFullError('mobile', e, verbose)); return null
|
||||||
})
|
})
|
||||||
if (desktopResult) {
|
if (mobileResult) {
|
||||||
desktopInitial = desktopResult.initialPoints
|
mobileInitial = mobileResult.initialPoints
|
||||||
desktopCollected = desktopResult.collectedPoints
|
mobileCollected = mobileResult.collectedPoints
|
||||||
}
|
|
||||||
this.isDesktopRunning = false
|
|
||||||
|
|
||||||
// If banned or compromised detected, skip mobile to save time
|
|
||||||
if (!banned.status && !this.compromisedModeActive) {
|
|
||||||
this.isMobile = true
|
|
||||||
this.isMobileRunning = true
|
|
||||||
const mobileResult = await this.Mobile(account).catch((e: unknown) => {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
|
||||||
this.recordRiskEvent('error', 6, `mobile:${msg}`)
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
|
||||||
const bd = detectBanReason(e)
|
|
||||||
if (bd.status) {
|
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
|
||||||
this.recordRiskEvent('ban_hint', 9, bd.reason)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
|
||||||
}
|
|
||||||
errors.push(formatFullError('mobile', e, verbose)); return null
|
|
||||||
})
|
|
||||||
if (mobileResult) {
|
|
||||||
mobileInitial = mobileResult.initialPoints
|
|
||||||
mobileCollected = mobileResult.collectedPoints
|
|
||||||
}
|
|
||||||
this.isMobileRunning = false
|
|
||||||
} else {
|
|
||||||
const why = banned.status ? 'banned status' : 'compromised status'
|
|
||||||
log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn')
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const why = banned.status ? 'banned status' : 'compromised status'
|
||||||
|
log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user