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:
2025-11-03 21:21:13 +01:00
parent f1db62823c
commit 39b62a4190
9 changed files with 318 additions and 340 deletions

View File

@@ -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))
}

View File

@@ -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)
}
}

View File

@@ -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'))
}
}

View File

@@ -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