mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
feat: Refactor compromised mode handling and improve error logging across flows
This commit is contained in:
@@ -18,11 +18,10 @@
|
||||
* node setup/update/update.mjs --docker # Update Docker containers
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, cpSync, rmSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
import { execSync, spawn } from 'node:child_process'
|
||||
import { cpSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { get as httpsGet } from 'node:https'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
function stripJsonComments(input) {
|
||||
let result = ""
|
||||
@@ -286,11 +285,16 @@ async function updateGit() {
|
||||
|
||||
if (!behindCount || behindCount === '0') {
|
||||
console.log('✓ Already up to date!')
|
||||
// FIXED: Return 0 but DON'T create update marker (no restart needed)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log(`ℹ️ ${behindCount} commits behind remote`)
|
||||
|
||||
// MARK: Update is happening - create marker file for bot to detect
|
||||
const updateMarkerPath = join(process.cwd(), '.update-happened')
|
||||
writeFileSync(updateMarkerPath, `Updated from ${currentCommit} to latest at ${new Date().toISOString()}`)
|
||||
|
||||
// Use merge with strategy to accept remote changes for all files
|
||||
// We'll restore user files afterwards
|
||||
const mergeCode = await run('git', ['merge', '--strategy-option=theirs', remoteBranch])
|
||||
@@ -541,6 +545,11 @@ async function updateGitFree() {
|
||||
rmSync(extractDir, { recursive: true, force: true })
|
||||
console.log('✓ Cleanup complete')
|
||||
|
||||
// MARK: Update happened - create marker file for bot to detect restart
|
||||
const updateMarkerPath = join(process.cwd(), '.update-happened')
|
||||
writeFileSync(updateMarkerPath, `Git-free update completed at ${new Date().toISOString()}`)
|
||||
console.log('✓ Created update marker for bot restart detection')
|
||||
|
||||
// Step 9: Install & build
|
||||
const hasNpm = await which('npm')
|
||||
if (!hasNpm) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { Account } from '../interface/Account'
|
||||
import { createBrowserInstance } from '../util/BrowserFactory'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface DesktopFlowResult {
|
||||
initialPoints: number
|
||||
@@ -66,32 +66,9 @@ export class DesktopFlow {
|
||||
await this.bot.login.login(this.bot.homePage, account.email, account.password, account.totp)
|
||||
|
||||
if (this.bot.compromisedModeActive) {
|
||||
// User wants the page to remain open for manual recovery. Do not proceed to tasks.
|
||||
keepBrowserOpen = true
|
||||
const reason = this.bot.compromisedReason || 'security-issue'
|
||||
this.bot.log(false, 'DESKTOP-FLOW', `Account security check failed (${reason}). Browser kept open for manual review: ${account.email}`, 'warn', 'yellow')
|
||||
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.bot.config,
|
||||
'🔐 Security Check',
|
||||
`**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, activities paused`,
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(false, 'DESKTOP-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
// Save session for convenience, but do not close the browser
|
||||
try {
|
||||
await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, false)
|
||||
} catch (e) {
|
||||
this.bot.log(false, 'DESKTOP-FLOW', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||
}
|
||||
|
||||
const result = await handleCompromisedMode(this.bot, account.email, reason, false)
|
||||
keepBrowserOpen = result.keepBrowserOpen
|
||||
return { initialPoints: 0, collectedPoints: 0 }
|
||||
}
|
||||
|
||||
|
||||
63
src/flows/FlowUtils.ts
Normal file
63
src/flows/FlowUtils.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Shared utilities for Desktop and Mobile flows
|
||||
* Extracts common patterns to reduce code duplication
|
||||
*/
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
|
||||
/**
|
||||
* Handle compromised/security check mode for an account
|
||||
* Sends security alert webhook, saves session, and keeps browser open for manual review
|
||||
*
|
||||
* @param bot Bot instance
|
||||
* @param account Email of affected account
|
||||
* @param reason Reason for security check (e.g., 'recovery-email-mismatch', '2fa-required')
|
||||
* @param isMobile Whether this is mobile flow (affects logging context)
|
||||
* @returns Object with keepBrowserOpen flag (always true for compromised mode)
|
||||
*
|
||||
* @example
|
||||
* const result = await handleCompromisedMode(bot, 'user@example.com', 'recovery-mismatch', false)
|
||||
* if (result.keepBrowserOpen) return { initialPoints: 0, collectedPoints: 0 }
|
||||
*/
|
||||
export async function handleCompromisedMode(
|
||||
bot: MicrosoftRewardsBot,
|
||||
account: string,
|
||||
reason: string,
|
||||
isMobile: boolean
|
||||
): Promise<{ keepBrowserOpen: boolean }> {
|
||||
const flowContext = isMobile ? 'MOBILE-FLOW' : 'DESKTOP-FLOW'
|
||||
|
||||
bot.log(
|
||||
isMobile,
|
||||
flowContext,
|
||||
`Account security check failed (${reason}). Browser kept open for manual review: ${account}`,
|
||||
'warn',
|
||||
'yellow'
|
||||
)
|
||||
|
||||
// Send security alert webhook
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
bot.config,
|
||||
isMobile ? '🔐 Security Check (Mobile)' : '🔐 Security Check',
|
||||
`**Account:** ${account}\n**Status:** ${reason}\n**Action:** Browser kept open, ${isMobile ? 'mobile ' : ''}activities paused`,
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
bot.log(isMobile, flowContext, `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
// Save session for convenience (non-critical)
|
||||
try {
|
||||
await saveSessionData(bot.config.sessionPath, bot.homePage.context(), account, isMobile)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
bot.log(isMobile, flowContext, `Failed to save session: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
return { keepBrowserOpen: true }
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
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'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface MobileFlowResult {
|
||||
initialPoints: number
|
||||
@@ -73,30 +73,9 @@ export class MobileFlow {
|
||||
await this.bot.login.login(this.bot.homePage, account.email, account.password, account.totp)
|
||||
|
||||
if (this.bot.compromisedModeActive) {
|
||||
keepBrowserOpen = true
|
||||
const reason = this.bot.compromisedReason || 'security-issue'
|
||||
this.bot.log(true, 'MOBILE-FLOW', `Mobile security check failed (${reason}). Browser kept open for manual review: ${account.email}`, 'warn', 'yellow')
|
||||
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.bot.config,
|
||||
'🔐 Security Check (Mobile)',
|
||||
`**Account:** ${account.email}\n**Status:** ${reason}\n**Action:** Browser kept open, mobile activities paused`,
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(true, 'MOBILE-FLOW', `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
try {
|
||||
await saveSessionData(this.bot.config.sessionPath, this.bot.homePage.context(), account.email, true)
|
||||
} catch (e) {
|
||||
this.bot.log(true, 'MOBILE-FLOW', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||
}
|
||||
|
||||
const result = await handleCompromisedMode(this.bot, account.email, reason, true)
|
||||
keepBrowserOpen = result.keepBrowserOpen
|
||||
return { initialPoints: 0, collectedPoints: 0 }
|
||||
}
|
||||
|
||||
|
||||
@@ -92,13 +92,14 @@ export class Activities {
|
||||
await this.doUrlReward(page)
|
||||
break
|
||||
case 'unsupported':
|
||||
// FIXED: Added explicit default case
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${String((activity as { promotionType?: string }).promotionType)}"!`, 'warn')
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${(activity as { promotionType?: string }).promotionType || 'unknown'}"`, 'warn')
|
||||
break
|
||||
default:
|
||||
// Exhaustiveness check - should never reach here due to ActivityKind type
|
||||
default: {
|
||||
// Exhaustiveness check - TypeScript ensures all ActivityKind types are handled
|
||||
const _exhaustive: never = kind
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Unexpected activity kind for "${activity.title}"`, 'error')
|
||||
break
|
||||
return _exhaustive
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Dispatcher error for "${activity.title}": ${e instanceof Error ? e.message : e}`, 'error')
|
||||
|
||||
@@ -237,26 +237,21 @@ export class Workers {
|
||||
await page.click(selector, { timeout: 10000 })
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
// FIXED: Use AbortController for proper cancellation instead of race condition
|
||||
// Execute activity with timeout protection using Promise.race
|
||||
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
|
||||
const controller = new AbortController()
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
controller.abort()
|
||||
}, timeoutMs)
|
||||
|
||||
const runWithTimeout = async (p: Promise<void>) => {
|
||||
try {
|
||||
await p
|
||||
clearTimeout(timeoutHandle)
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutHandle)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
await retry.run(async () => {
|
||||
const activityPromise = this.bot.activities.run(page, activity)
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))
|
||||
}, timeoutMs)
|
||||
// Clean up timer if activity completes first
|
||||
activityPromise.finally(() => clearTimeout(timer))
|
||||
})
|
||||
|
||||
try {
|
||||
await runWithTimeout(this.bot.activities.run(page, activity))
|
||||
await Promise.race([activityPromise, timeoutPromise])
|
||||
throttle.record(true)
|
||||
} catch (e) {
|
||||
throttle.record(false)
|
||||
|
||||
55
src/index.ts
55
src/index.ts
@@ -16,7 +16,7 @@ import { log } from './util/Logger'
|
||||
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
||||
import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
||||
import { StartupValidator } from './util/StartupValidator'
|
||||
import { Util } from './util/Utils'
|
||||
import { formatDetailedError, shortErrorMessage, Util } from './util/Utils'
|
||||
|
||||
import { Activities } from './functions/Activities'
|
||||
import { Login } from './functions/Login'
|
||||
@@ -877,20 +877,9 @@ function isWorkerMessage(msg: unknown): msg is WorkerMessage {
|
||||
return m.type === 'summary' && Array.isArray(m.data)
|
||||
}
|
||||
|
||||
function shortErr(e: unknown): string {
|
||||
if (e == null) return 'unknown'
|
||||
if (e instanceof Error) return e.message.substring(0, 120)
|
||||
const s = String(e)
|
||||
return s.substring(0, 120)
|
||||
}
|
||||
|
||||
function formatFullError(label: string, e: unknown, verbose: boolean): string {
|
||||
const base = shortErr(e)
|
||||
if (verbose && e instanceof Error && e.stack) {
|
||||
return `${label}:${base} :: ${e.stack.split('\n').slice(0, 4).join(' | ')}`
|
||||
}
|
||||
return `${label}:${base}`
|
||||
}
|
||||
// Use utility functions from Utils.ts
|
||||
const shortErr = shortErrorMessage
|
||||
const formatFullError = formatDetailedError
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// Check for dashboard mode flag (standalone dashboard)
|
||||
@@ -981,23 +970,41 @@ async function main(): Promise<void> {
|
||||
const bootstrap = async () => {
|
||||
try {
|
||||
// Check for updates BEFORE initializing and running tasks
|
||||
// CRITICAL: Only restart if update script explicitly indicates new version was installed
|
||||
try {
|
||||
const updateResult = await rewardsBot.runAutoUpdate().catch((e) => {
|
||||
log('main', 'UPDATE', `Auto-update check failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||
return -1
|
||||
})
|
||||
|
||||
// FIXED: Only restart on exit code 0 AND if update actually happened
|
||||
// The update script returns 0 even when no update is needed, which causes infinite loop
|
||||
// Solution: Check for marker file that update script creates when actual update happens
|
||||
if (updateResult === 0) {
|
||||
log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
|
||||
const updateMarkerPath = path.join(process.cwd(), '.update-happened')
|
||||
const updateHappened = fs.existsSync(updateMarkerPath)
|
||||
|
||||
// Restart the process with the same arguments
|
||||
const { spawn } = await import('child_process')
|
||||
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||
detached: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
child.unref()
|
||||
process.exit(0)
|
||||
if (updateHappened) {
|
||||
// Remove marker file
|
||||
try {
|
||||
fs.unlinkSync(updateMarkerPath)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
|
||||
|
||||
// Restart the process with the same arguments
|
||||
const { spawn } = await import('child_process')
|
||||
const child = spawn(process.execPath, process.argv.slice(1), {
|
||||
detached: true,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
child.unref()
|
||||
process.exit(0)
|
||||
} else {
|
||||
log('main', 'UPDATE', 'Already up to date, continuing with bot execution')
|
||||
}
|
||||
}
|
||||
} catch (updateError) {
|
||||
log('main', 'UPDATE', `Update check failed (continuing): ${updateError instanceof Error ? updateError.message : String(updateError)}`, 'warn')
|
||||
|
||||
@@ -169,4 +169,36 @@ export class Util {
|
||||
return milisec
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract short error message from unknown error type (max 120 chars)
|
||||
* @param error - Error object or unknown value
|
||||
* @returns Truncated string representation of the error
|
||||
* @example shortErrorMessage(new Error('Something went wrong')) // 'Something went wrong'
|
||||
* @example shortErrorMessage(null) // 'unknown'
|
||||
*/
|
||||
export function shortErrorMessage(error: unknown): string {
|
||||
if (error == null) return 'unknown'
|
||||
if (error instanceof Error) return error.message.substring(0, 120)
|
||||
const str = String(error)
|
||||
return str.substring(0, 120)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format detailed error message with optional stack trace
|
||||
* @param label - Error context label (e.g., 'desktop', 'mobile', 'login')
|
||||
* @param error - Error object or unknown value
|
||||
* @param includeStack - Whether to include stack trace (default: false)
|
||||
* @returns Formatted error string with label and optionally stack trace
|
||||
* @example formatDetailedError('desktop', new Error('Failed'), true) // 'desktop:Failed :: at line1 | at line2...'
|
||||
* @example formatDetailedError('mobile', 'timeout') // 'mobile:timeout'
|
||||
*/
|
||||
export function formatDetailedError(label: string, error: unknown, includeStack: boolean = false): string {
|
||||
const baseMessage = shortErrorMessage(error)
|
||||
if (includeStack && error instanceof Error && error.stack) {
|
||||
const stackLines = error.stack.split('\n').slice(0, 4).join(' | ')
|
||||
return `${label}:${baseMessage} :: ${stackLines}`
|
||||
}
|
||||
return `${label}:${baseMessage}`
|
||||
}
|
||||
Reference in New Issue
Block a user