mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 17:56:15 +00:00
feat: Implement account suspension checks and improve error handling
- Added a method to check for account suspension using multiple detection methods in BrowserFunc. - Refactored existing suspension checks to utilize the new method, reducing code duplication. - Enhanced error handling in various functions to throw original errors instead of wrapping them. - Improved environment variable parsing in constants to streamline validation. - Updated login flow to optimize session restoration and error handling. - Refined Axios request logic to include retry mechanisms for proxy authentication and network errors. - Enhanced logging functionality to provide clearer output and error context. - Improved utility functions with additional validation for input parameters.
This commit is contained in:
@@ -22,7 +22,7 @@ class AxiosClient {
|
||||
|
||||
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
|
||||
const { proxyUrl, protocol } = this.buildProxyUrl(proxyConfig)
|
||||
const normalized = protocol.replace(/:$/, '')
|
||||
const normalized = protocol.replace(/:$/, '').toLowerCase()
|
||||
|
||||
switch (normalized) {
|
||||
case 'http':
|
||||
@@ -80,7 +80,7 @@ class AxiosClient {
|
||||
return { proxyUrl: parsedUrl.toString(), protocol: parsedUrl.protocol }
|
||||
}
|
||||
|
||||
// Generic method to make any Axios request
|
||||
// Generic method to make any Axios request with retry logic
|
||||
public async request(config: AxiosRequestConfig, bypassProxy = false): Promise<AxiosResponse> {
|
||||
if (bypassProxy) {
|
||||
const bypassInstance = axios.create()
|
||||
@@ -95,25 +95,16 @@ class AxiosClient {
|
||||
return await this.instance.request(config)
|
||||
} catch (err: unknown) {
|
||||
lastError = err
|
||||
const axiosErr = err as AxiosError | undefined
|
||||
|
||||
// Detect HTTP proxy auth failures (status 407) and retry without proxy
|
||||
if (axiosErr && axiosErr.response && axiosErr.response.status === 407) {
|
||||
if (attempt < maxAttempts) {
|
||||
await this.sleep(1000 * attempt) // Exponential backoff
|
||||
}
|
||||
|
||||
// Handle HTTP 407 Proxy Authentication Required
|
||||
if (this.isProxyAuthError(err)) {
|
||||
// Retry without proxy on auth failure
|
||||
const bypassInstance = axios.create()
|
||||
return bypassInstance.request(config)
|
||||
}
|
||||
|
||||
// If proxied request fails with common proxy/network errors, retry with backoff
|
||||
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
|
||||
const code = e?.code || e?.cause?.code
|
||||
const isNetErr = code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ENOTFOUND'
|
||||
const msg = String(e?.message || '')
|
||||
const looksLikeProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
||||
|
||||
if (isNetErr || looksLikeProxyIssue) {
|
||||
// Handle retryable network errors
|
||||
if (this.isRetryableError(err)) {
|
||||
if (attempt < maxAttempts) {
|
||||
// Exponential backoff: 1s, 2s, 4s, etc.
|
||||
const delayMs = 1000 * Math.pow(2, attempt - 1)
|
||||
@@ -133,6 +124,34 @@ class AxiosClient {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is HTTP 407 Proxy Authentication Required
|
||||
*/
|
||||
private isProxyAuthError(err: unknown): boolean {
|
||||
const axiosErr = err as AxiosError | undefined
|
||||
return axiosErr?.response?.status === 407
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is retryable (network/proxy issues)
|
||||
*/
|
||||
private isRetryableError(err: unknown): boolean {
|
||||
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
|
||||
if (!e) return false
|
||||
|
||||
const code = e.code || e.cause?.code
|
||||
const isNetworkError = code === 'ECONNREFUSED' ||
|
||||
code === 'ETIMEDOUT' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'ENOTFOUND' ||
|
||||
code === 'EPIPE'
|
||||
|
||||
const msg = String(e.message || '')
|
||||
const isProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
||||
|
||||
return isNetworkError || isProxyIssue
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ type WebhookBuffer = {
|
||||
const webhookBuffers = new Map<string, WebhookBuffer>()
|
||||
|
||||
// Periodic cleanup of old/idle webhook buffers to prevent memory leaks
|
||||
setInterval(() => {
|
||||
const BUFFER_MAX_AGE_MS = 3600000 // 1 hour
|
||||
const BUFFER_CLEANUP_INTERVAL_MS = 600000 // 10 minutes
|
||||
|
||||
const cleanupInterval = setInterval(() => {
|
||||
const now = Date.now()
|
||||
const BUFFER_MAX_AGE_MS = 3600000 // 1 hour
|
||||
|
||||
for (const [url, buf] of webhookBuffers.entries()) {
|
||||
if (!buf.sending && buf.lines.length === 0) {
|
||||
@@ -28,7 +30,12 @@ setInterval(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 600000) // Check every 10 minutes
|
||||
}, BUFFER_CLEANUP_INTERVAL_MS)
|
||||
|
||||
// Allow cleanup to be stopped (prevents process from hanging)
|
||||
if (cleanupInterval.unref) {
|
||||
cleanupInterval.unref()
|
||||
}
|
||||
|
||||
function getBuffer(url: string): WebhookBuffer {
|
||||
let buf = webhookBuffers.get(url)
|
||||
@@ -87,28 +94,25 @@ async function sendBatch(url: string, buf: WebhookBuffer) {
|
||||
|
||||
function determineColorFromContent(content: string): number {
|
||||
const lower = content.toLowerCase()
|
||||
// Security/Ban alerts - Red
|
||||
|
||||
// Priority order: most critical first
|
||||
if (lower.includes('[banned]') || lower.includes('[security]') || lower.includes('suspended') || lower.includes('compromised')) {
|
||||
return DISCORD.COLOR_RED
|
||||
}
|
||||
// Errors - Dark Red
|
||||
if (lower.includes('[error]') || lower.includes('✗')) {
|
||||
return DISCORD.COLOR_CRIMSON
|
||||
}
|
||||
// Warnings - Orange/Yellow
|
||||
if (lower.includes('[warn]') || lower.includes('⚠')) {
|
||||
return DISCORD.COLOR_ORANGE
|
||||
}
|
||||
// Success - Green
|
||||
if (lower.includes('[ok]') || lower.includes('✓') || lower.includes('complet')) {
|
||||
return DISCORD.COLOR_GREEN
|
||||
}
|
||||
// Info/Main - Blue
|
||||
if (lower.includes('[main]')) {
|
||||
return DISCORD.COLOR_BLUE
|
||||
}
|
||||
// Default - Gray
|
||||
return 0x95A5A6 // Gray
|
||||
|
||||
return 0x95A5A6
|
||||
}
|
||||
|
||||
function enqueueWebhookLog(url: string, line: string) {
|
||||
@@ -246,7 +250,6 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
|
||||
// Return an Error when logging an error so callers can `throw log(...)`
|
||||
if (type === 'error') {
|
||||
// CommunityReporter disabled per project policy
|
||||
return new Error(cleanStr)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
import chalk from 'chalk'
|
||||
import { Config } from '../interface/Config'
|
||||
import { Account } from '../interface/Account'
|
||||
import { log } from './Logger'
|
||||
|
||||
interface ValidationError {
|
||||
severity: 'error' | 'warning'
|
||||
@@ -22,9 +23,7 @@ export class StartupValidator {
|
||||
* Displays errors and warnings but lets execution continue.
|
||||
*/
|
||||
async validate(config: Config, accounts: Account[]): Promise<boolean> {
|
||||
console.log(chalk.cyan('\n═══════════════════════════════════════════════════════════════'))
|
||||
console.log(chalk.cyan(' 🔍 STARTUP VALIDATION - Checking Configuration'))
|
||||
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════\n'))
|
||||
log('main', 'STARTUP', 'Running configuration validation...')
|
||||
|
||||
// Run all validation checks
|
||||
this.validateAccounts(accounts)
|
||||
@@ -621,62 +620,45 @@ export class StartupValidator {
|
||||
}
|
||||
|
||||
private async displayResults(): Promise<void> {
|
||||
// Display errors
|
||||
if (this.errors.length > 0) {
|
||||
console.log(chalk.red('\n❌ VALIDATION ERRORS FOUND:\n'))
|
||||
log('main', 'VALIDATION', chalk.red('❌ VALIDATION ERRORS FOUND:'), 'error')
|
||||
this.errors.forEach((err, index) => {
|
||||
console.log(chalk.red(` ${index + 1}. [${err.category.toUpperCase()}] ${err.message}`))
|
||||
log('main', 'VALIDATION', chalk.red(`${index + 1}. [${err.category.toUpperCase()}] ${err.message}`), 'error')
|
||||
if (err.fix) {
|
||||
console.log(chalk.yellow(` 💡 Fix: ${err.fix}`))
|
||||
log('main', 'VALIDATION', chalk.yellow(` Fix: ${err.fix}`), 'warn')
|
||||
}
|
||||
if (err.docsLink) {
|
||||
console.log(chalk.cyan(` 📖 Documentation: ${err.docsLink}`))
|
||||
log('main', 'VALIDATION', ` Docs: ${err.docsLink}`)
|
||||
}
|
||||
console.log('')
|
||||
})
|
||||
}
|
||||
|
||||
// Display warnings
|
||||
if (this.warnings.length > 0) {
|
||||
console.log(chalk.yellow('\n⚠️ WARNINGS:\n'))
|
||||
log('main', 'VALIDATION', chalk.yellow('⚠️ WARNINGS:'), 'warn')
|
||||
this.warnings.forEach((warn, index) => {
|
||||
console.log(chalk.yellow(` ${index + 1}. [${warn.category.toUpperCase()}] ${warn.message}`))
|
||||
log('main', 'VALIDATION', chalk.yellow(`${index + 1}. [${warn.category.toUpperCase()}] ${warn.message}`), 'warn')
|
||||
if (warn.fix) {
|
||||
console.log(chalk.gray(` 💡 Suggestion: ${warn.fix}`))
|
||||
log('main', 'VALIDATION', ` Suggestion: ${warn.fix}`)
|
||||
}
|
||||
if (warn.docsLink) {
|
||||
console.log(chalk.cyan(` 📖 Documentation: ${warn.docsLink}`))
|
||||
log('main', 'VALIDATION', ` Docs: ${warn.docsLink}`)
|
||||
}
|
||||
console.log('')
|
||||
})
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════'))
|
||||
|
||||
if (this.errors.length === 0 && this.warnings.length === 0) {
|
||||
console.log(chalk.green(' ✅ All validation checks passed! Configuration looks good.'))
|
||||
console.log(chalk.gray(' → Starting bot execution...'))
|
||||
log('main', 'VALIDATION', chalk.green('✅ All validation checks passed!'))
|
||||
} else {
|
||||
console.log(chalk.white(` Found: ${chalk.red(`${this.errors.length} error(s)`)} | ${chalk.yellow(`${this.warnings.length} warning(s)`)}`))
|
||||
log('main', 'VALIDATION', `Found: ${this.errors.length} error(s) | ${this.warnings.length} warning(s)`)
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
console.log(chalk.red('\n ⚠️ CRITICAL ERRORS DETECTED'))
|
||||
console.log(chalk.white(' → Bot will continue, but these issues may cause failures'))
|
||||
console.log(chalk.white(' → Review errors above and fix them for stable operation'))
|
||||
console.log(chalk.gray(' → If you believe these are false positives, you can ignore them'))
|
||||
log('main', 'VALIDATION', 'Bot will continue, but issues may cause failures', 'warn')
|
||||
} else {
|
||||
console.log(chalk.yellow('\n ⚠️ Warnings detected - review recommended'))
|
||||
console.log(chalk.gray(' → Bot will continue normally'))
|
||||
log('main', 'VALIDATION', 'Warnings detected - review recommended', 'warn')
|
||||
}
|
||||
|
||||
console.log(chalk.white('\n 📖 Full documentation: docs/index.md'))
|
||||
console.log(chalk.gray(' → Proceeding with execution in 5 seconds...'))
|
||||
|
||||
// Give user time to read (5 seconds for errors, 5 seconds for warnings)
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
log('main', 'VALIDATION', 'Full documentation: docs/index.md')
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('═══════════════════════════════════════════════════════════════\n'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,30 @@ import ms from 'ms'
|
||||
export class Util {
|
||||
|
||||
async wait(ms: number): Promise<void> {
|
||||
// Safety check: prevent extremely long or negative waits
|
||||
const MAX_WAIT_MS = 3600000 // 1 hour max
|
||||
const safeMs = Math.min(Math.max(0, ms), MAX_WAIT_MS)
|
||||
const MAX_WAIT_MS = 3600000 // 1 hour max to prevent infinite waits
|
||||
const MIN_WAIT_MS = 0
|
||||
|
||||
if (ms !== safeMs) {
|
||||
console.warn(`[Utils] wait() clamped from ${ms}ms to ${safeMs}ms (max: ${MAX_WAIT_MS}ms)`)
|
||||
// Validate and clamp input
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error(`Invalid wait time: ${ms}. Must be a finite number.`)
|
||||
}
|
||||
|
||||
const safeMs = Math.min(Math.max(MIN_WAIT_MS, ms), MAX_WAIT_MS)
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, safeMs)
|
||||
})
|
||||
}
|
||||
|
||||
async waitRandom(minMs: number, maxMs: number): Promise<void> {
|
||||
if (!Number.isFinite(minMs) || !Number.isFinite(maxMs)) {
|
||||
throw new Error(`Invalid wait range: min=${minMs}, max=${maxMs}. Both must be finite numbers.`)
|
||||
}
|
||||
|
||||
if (minMs > maxMs) {
|
||||
throw new Error(`Invalid wait range: min (${minMs}) cannot be greater than max (${maxMs}).`)
|
||||
}
|
||||
|
||||
const delta = this.randomNumber(minMs, maxMs)
|
||||
return this.wait(delta)
|
||||
}
|
||||
@@ -37,13 +47,25 @@ export class Util {
|
||||
}
|
||||
|
||||
randomNumber(min: number, max: number): number {
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
throw new Error(`Invalid range: min=${min}, max=${max}. Both must be finite numbers.`)
|
||||
}
|
||||
|
||||
if (min > max) {
|
||||
throw new Error(`Invalid range: min (${min}) cannot be greater than max (${max}).`)
|
||||
}
|
||||
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
||||
// Validate input to prevent division by zero or invalid chunks
|
||||
if (numChunks <= 0) {
|
||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive integer.`)
|
||||
if (!Number.isFinite(numChunks) || numChunks <= 0) {
|
||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
|
||||
}
|
||||
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new Error('Invalid input: arr must be an array.')
|
||||
}
|
||||
|
||||
if (arr.length === 0) {
|
||||
@@ -63,8 +85,12 @@ export class Util {
|
||||
}
|
||||
|
||||
stringToMs(input: string | number): number {
|
||||
if (typeof input !== 'string' && typeof input !== 'number') {
|
||||
throw new Error('Invalid input type. Expected string or number.')
|
||||
}
|
||||
|
||||
const milisec = ms(input.toString())
|
||||
if (!milisec) {
|
||||
if (!milisec || !Number.isFinite(milisec)) {
|
||||
throw new Error('The string provided cannot be parsed to a valid time! Use a format like "1 min", "1m" or "1 minutes"')
|
||||
}
|
||||
return milisec
|
||||
|
||||
Reference in New Issue
Block a user