New structure

This commit is contained in:
2025-11-11 12:59:42 +01:00
parent 088a3a024f
commit 89bc226d6b
46 changed files with 990 additions and 944 deletions

View File

@@ -52,16 +52,12 @@ browser/
.eslintcache .eslintcache
setup/ setup/
# Docker files (no recursion) # Docker files (organized in docker/ folder - no recursion needed)
Dockerfile docker/
docker-compose.yml
compose.yaml
.dockerignore .dockerignore
# NixOS specific files (not needed in Docker) # Scripts (organized in scripts/ folder - not needed in Docker)
flake.nix scripts/
flake.lock
run.sh
# Asset files (not needed for runtime) # Asset files (not needed for runtime)
assets/ assets/

View File

@@ -44,17 +44,33 @@ src/
│ ├── Poll.ts # Poll completion │ ├── Poll.ts # Poll completion
│ ├── ThisOrThat.ts # This or That game │ ├── ThisOrThat.ts # This or That game
│ └── ... │ └── ...
├── util/ # Shared utilities + infrastructure ├── util/ # Shared utilities (ORGANIZED BY CATEGORY)
│ ├── Axios.ts # HTTP client with proxy support │ ├── core/ # Core utilities
│ ├── BrowserFactory.ts # Centralized browser creation │ ├── Utils.ts # General-purpose helpers
├── Humanizer.ts # Random delays, mouse gestures │ └── Retry.ts # Exponential backoff retry logic
│ ├── BanDetector.ts # Heuristic ban detection │ ├── network/ # HTTP & API utilities
│ ├── QueryDiversityEngine.ts # Multi-source search query generation │ ├── Axios.ts # HTTP client with proxy support
├── JobState.ts # Persistent job state tracking │ └── QueryDiversityEngine.ts # Multi-source search query generation
│ ├── Logger.ts # Centralized logging with redaction │ ├── browser/ # Browser automation utilities
│ ├── Retry.ts # Exponential backoff retry logic │ ├── BrowserFactory.ts # Centralized browser creation
│ ├── Utils.ts # General-purpose helpers │ ├── Humanizer.ts # Random delays, mouse gestures
│ └── ... │ └── UserAgent.ts # User agent generation
│ ├── state/ # State & persistence
│ │ ├── JobState.ts # Persistent job state tracking
│ │ ├── Load.ts # Configuration & session loading
│ │ └── MobileRetryTracker.ts # Mobile search retry tracking
│ ├── validation/ # Validation & detection
│ │ ├── StartupValidator.ts # Comprehensive startup validation
│ │ ├── BanDetector.ts # Heuristic ban detection
│ │ └── LoginStateDetector.ts # Login state detection
│ ├── security/ # Authentication & security
│ │ └── Totp.ts # TOTP generation for 2FA
│ └── notifications/ # Logging & notifications
│ ├── Logger.ts # Centralized logging with redaction
│ ├── ConclusionWebhook.ts # Summary webhook notifications
│ ├── ErrorReportingWebhook.ts # Error reporting
│ ├── Ntfy.ts # Push notifications
│ └── AdaptiveThrottler.ts # Adaptive delay management
├── dashboard/ # Real-time web dashboard (Express + WebSocket) ├── dashboard/ # Real-time web dashboard (Express + WebSocket)
│ ├── server.ts # Express server + routes │ ├── server.ts # Express server + routes
│ ├── routes.ts # API endpoints │ ├── routes.ts # API endpoints
@@ -73,9 +89,20 @@ src/
├── nameDatabase.ts # First/last name pool ├── nameDatabase.ts # First/last name pool
├── types.ts # Account creation interfaces ├── types.ts # Account creation interfaces
└── README.md # Account creation guide └── README.md # Account creation guide
docker/ # Docker deployment files
├── Dockerfile # Multi-stage Docker build
├── compose.yaml # Docker Compose configuration
├── entrypoint.sh # Container initialization script
├── run_daily.sh # Daily execution wrapper (cron)
└── crontab.template # Cron schedule template
scripts/ # Utility scripts
└── run.sh # Nix development environment launcher
setup/ setup/
├── setup.bat # Windows setup script ├── setup.bat # Windows setup script
├── setup.sh # Linux/Mac setup script ├── setup.sh # Linux/Mac setup script
├── nix/ # NixOS configuration
│ ├── flake.nix # Nix flake definition
│ └── flake.lock # Nix flake lock file
└── update/ └── update/
├── setup.mjs # Initial setup automation ├── setup.mjs # Initial setup automation
└── update.mjs # GitHub ZIP-based auto-updater (NO GIT REQUIRED!) └── update.mjs # GitHub ZIP-based auto-updater (NO GIT REQUIRED!)
@@ -986,8 +1013,8 @@ private combinedDeduplication(queries: string[], threshold = 0.65): string[] {
### Docker & Scheduling Context ### Docker & Scheduling Context
**entrypoint.sh:** **docker/entrypoint.sh:**
- **Purpose:** Docker container initialization script - **Purpose:** Docker container initialization script (located in `docker/` directory)
- **Key Features:** - **Key Features:**
- Timezone configuration (env: `TZ`, default UTC) - Timezone configuration (env: `TZ`, default UTC)
- Initial run on start (env: `RUN_ON_START=true`) - Initial run on start (env: `RUN_ON_START=true`)
@@ -995,8 +1022,8 @@ private combinedDeduplication(queries: string[], threshold = 0.65): string[] {
- Playwright browser preinstallation (`PLAYWRIGHT_BROWSERS_PATH=0`) - Playwright browser preinstallation (`PLAYWRIGHT_BROWSERS_PATH=0`)
- **Usage:** Docker Compose sets `CRON_SCHEDULE`, container runs cron in foreground - **Usage:** Docker Compose sets `CRON_SCHEDULE`, container runs cron in foreground
**run_daily.sh:** **docker/run_daily.sh:**
- **Purpose:** Daily execution wrapper for cron jobs - **Purpose:** Daily execution wrapper for cron jobs (located in `docker/` directory)
- **Key Features:** - **Key Features:**
- Random sleep delay (0-30min) to avoid simultaneous runs across containers - Random sleep delay (0-30min) to avoid simultaneous runs across containers
- Environment variable: `SKIP_RANDOM_SLEEP=true` to disable delay - Environment variable: `SKIP_RANDOM_SLEEP=true` to disable delay

View File

@@ -80,11 +80,12 @@ COPY --from=builder /usr/src/microsoft-rewards-bot/package*.json ./
COPY --from=builder /usr/src/microsoft-rewards-bot/node_modules ./node_modules COPY --from=builder /usr/src/microsoft-rewards-bot/node_modules ./node_modules
# Copy runtime scripts with proper permissions and normalize line endings for non-Unix users # Copy runtime scripts with proper permissions and normalize line endings for non-Unix users
COPY --chmod=755 src/run_daily.sh ./src/run_daily.sh # IMPROVED: Scripts now organized in docker/ folder
COPY --chmod=644 src/crontab.template /etc/cron.d/microsoft-rewards-cron.template COPY --chmod=755 docker/run_daily.sh ./docker/run_daily.sh
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh COPY --chmod=644 docker/crontab.template /etc/cron.d/microsoft-rewards-cron.template
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh \ RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh \
&& sed -i 's/\r$//' ./src/run_daily.sh && sed -i 's/\r$//' ./docker/run_daily.sh
# Entrypoint handles TZ, initial run toggle, cron templating & launch # Entrypoint handles TZ, initial run toggle, cron templating & launch
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -1,3 +1,3 @@
# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs # Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-bot/src/run_daily.sh >> /proc/1/fd/1 2>&1 ${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-bot/docker/run_daily.sh >> /proc/1/fd/1 2>&1

View File

@@ -26,7 +26,7 @@ if [ "${RUN_ON_START:-false}" = "true" ]; then
exit 1 exit 1
} }
# Skip random sleep for initial run, but preserve setting for cron jobs # Skip random sleep for initial run, but preserve setting for cron jobs
SKIP_RANDOM_SLEEP=true src/run_daily.sh SKIP_RANDOM_SLEEP=true docker/run_daily.sh
echo "[entrypoint-bg] Initial run completed at $(date)" echo "[entrypoint-bg] Initial run completed at $(date)"
) & ) &
echo "[entrypoint] Background process started (PID: $!)" echo "[entrypoint] Background process started (PID: $!)"

22
scripts/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Scripts Directory
This directory contains utility scripts for development and deployment.
## Available Scripts
### `run.sh`
**Purpose:** Nix development environment launcher
**Usage:** `./run.sh`
**Description:** Launches the bot using Nix develop environment with xvfb-run for headless browser support.
**Requirements:**
- Nix package manager
- xvfb (X Virtual Framebuffer)
**Environment:**
This script is designed for NixOS or systems with Nix installed. It provides a reproducible development environment as defined in `setup/nix/flake.nix`.
---
For Docker deployment, see the `docker/` directory.
For setup scripts, see the `setup/` directory.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import Browser from '../browser/Browser' import Browser from '../browser/Browser'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { log } from '../util/Logger' import { log } from '../util/notifications/Logger'
import { AccountCreator } from './AccountCreator' import { AccountCreator } from './AccountCreator'
async function main(): Promise<void> { async function main(): Promise<void> {
@@ -9,11 +9,11 @@ async function main(): Promise<void> {
let referralUrl: string | undefined let referralUrl: string | undefined
let recoveryEmail: string | undefined let recoveryEmail: string | undefined
let autoAccept = false let autoAccept = false
// Parse arguments - ULTRA SIMPLE // Parse arguments - ULTRA SIMPLE
for (const arg of args) { for (const arg of args) {
if (!arg) continue if (!arg) continue
if (arg === '-y' || arg === '--yes' || arg === 'y' || arg === 'Y') { if (arg === '-y' || arg === '--yes' || arg === 'y' || arg === 'Y') {
autoAccept = true autoAccept = true
} else if (arg.startsWith('http')) { } else if (arg.startsWith('http')) {
@@ -23,7 +23,7 @@ async function main(): Promise<void> {
recoveryEmail = arg recoveryEmail = arg
} }
} }
// Banner // Banner
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
@@ -34,18 +34,18 @@ async function main(): Promise<void> {
log(false, 'CREATOR-CLI', ' Only interact when explicitly asked (e.g., CAPTCHA solving).', 'warn', 'yellow') log(false, 'CREATOR-CLI', ' Only interact when explicitly asked (e.g., CAPTCHA solving).', 'warn', 'yellow')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Display detected arguments // Display detected arguments
if (referralUrl) { if (referralUrl) {
log(false, 'CREATOR-CLI', `✅ Referral URL: ${referralUrl}`, 'log', 'green') log(false, 'CREATOR-CLI', `✅ Referral URL: ${referralUrl}`, 'log', 'green')
} else { } else {
log(false, 'CREATOR-CLI', '⚠️ No referral URL - account will NOT be linked to rewards', 'warn', 'yellow') log(false, 'CREATOR-CLI', '⚠️ No referral URL - account will NOT be linked to rewards', 'warn', 'yellow')
} }
if (recoveryEmail) { if (recoveryEmail) {
log(false, 'CREATOR-CLI', `✅ Recovery email: ${recoveryEmail}`, 'log', 'green') log(false, 'CREATOR-CLI', `✅ Recovery email: ${recoveryEmail}`, 'log', 'green')
} }
if (autoAccept) { if (autoAccept) {
log(false, 'CREATOR-CLI', '⚡ Auto-accept mode ENABLED (-y flag detected)', 'log', 'green') log(false, 'CREATOR-CLI', '⚡ Auto-accept mode ENABLED (-y flag detected)', 'log', 'green')
log(false, 'CREATOR-CLI', '🤖 All prompts will be auto-accepted', 'log', 'cyan') log(false, 'CREATOR-CLI', '🤖 All prompts will be auto-accepted', 'log', 'cyan')
@@ -53,17 +53,17 @@ async function main(): Promise<void> {
log(false, 'CREATOR-CLI', '🤖 Interactive mode: you will be asked for options', 'log', 'cyan') log(false, 'CREATOR-CLI', '🤖 Interactive mode: you will be asked for options', 'log', 'cyan')
log(false, 'CREATOR-CLI', '💡 Tip: Use -y flag to auto-accept all prompts', 'log', 'gray') log(false, 'CREATOR-CLI', '💡 Tip: Use -y flag to auto-accept all prompts', 'log', 'gray')
} }
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Create a temporary bot instance to access browser creation // Create a temporary bot instance to access browser creation
const bot = new MicrosoftRewardsBot(false) const bot = new MicrosoftRewardsBot(false)
const browserFactory = new Browser(bot) const browserFactory = new Browser(bot)
try { try {
// Create browser (non-headless for user interaction with CAPTCHA) // Create browser (non-headless for user interaction with CAPTCHA)
log(false, 'CREATOR-CLI', 'Opening browser (required for CAPTCHA solving)...', 'log') log(false, 'CREATOR-CLI', 'Opening browser (required for CAPTCHA solving)...', 'log')
// Create empty proxy config (no proxy for account creation) // Create empty proxy config (no proxy for account creation)
const emptyProxy = { const emptyProxy = {
proxyAxios: false, proxyAxios: false,
@@ -72,44 +72,44 @@ async function main(): Promise<void> {
password: '', password: '',
username: '' username: ''
} }
const browserContext = await browserFactory.createBrowser(emptyProxy, 'account-creator') const browserContext = await browserFactory.createBrowser(emptyProxy, 'account-creator')
log(false, 'CREATOR-CLI', '✅ Browser opened successfully', 'log', 'green') log(false, 'CREATOR-CLI', '✅ Browser opened successfully', 'log', 'green')
// Create account // Create account
const creator = new AccountCreator(referralUrl, recoveryEmail, autoAccept) const creator = new AccountCreator(referralUrl, recoveryEmail, autoAccept)
const result = await creator.create(browserContext) const result = await creator.create(browserContext)
if (result) { if (result) {
// Success banner // Success banner
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '✅ ACCOUNT CREATED SUCCESSFULLY!', 'log', 'green') log(false, 'CREATOR-CLI', '✅ ACCOUNT CREATED SUCCESSFULLY!', 'log', 'green')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
// Display account details // Display account details
log(false, 'CREATOR-CLI', `📧 Email: ${result.email}`, 'log', 'cyan') log(false, 'CREATOR-CLI', `📧 Email: ${result.email}`, 'log', 'cyan')
log(false, 'CREATOR-CLI', `🔐 Password: ${result.password}`, 'log', 'cyan') log(false, 'CREATOR-CLI', `🔐 Password: ${result.password}`, 'log', 'cyan')
log(false, 'CREATOR-CLI', `👤 Name: ${result.firstName} ${result.lastName}`, 'log', 'cyan') log(false, 'CREATOR-CLI', `👤 Name: ${result.firstName} ${result.lastName}`, 'log', 'cyan')
log(false, 'CREATOR-CLI', `🎂 Birthdate: ${result.birthdate.day}/${result.birthdate.month}/${result.birthdate.year}`, 'log', 'cyan') log(false, 'CREATOR-CLI', `🎂 Birthdate: ${result.birthdate.day}/${result.birthdate.month}/${result.birthdate.year}`, 'log', 'cyan')
if (result.referralUrl) { if (result.referralUrl) {
log(false, 'CREATOR-CLI', '🔗 Referral: Linked', 'log', 'green') log(false, 'CREATOR-CLI', '🔗 Referral: Linked', 'log', 'green')
} }
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '💾 Account details saved to accounts-created/ directory', 'log', 'green') log(false, 'CREATOR-CLI', '💾 Account details saved to accounts-created/ directory', 'log', 'green')
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green') log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
// Keep browser open - don't close // Keep browser open - don't close
log(false, 'CREATOR-CLI', '✅ Account creation complete! Browser will remain open.', 'log', 'green') log(false, 'CREATOR-CLI', '✅ Account creation complete! Browser will remain open.', 'log', 'green')
log(false, 'CREATOR-CLI', 'You can now use the account or close the browser manually.', 'log', 'cyan') log(false, 'CREATOR-CLI', 'You can now use the account or close the browser manually.', 'log', 'cyan')
log(false, 'CREATOR-CLI', 'Press Ctrl+C to exit the script.', 'log', 'yellow') log(false, 'CREATOR-CLI', 'Press Ctrl+C to exit the script.', 'log', 'yellow')
// Keep process alive indefinitely // Keep process alive indefinitely
await new Promise(() => {}) // Never resolves await new Promise(() => { }) // Never resolves
} else { } else {
// Failure // Failure
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
@@ -117,11 +117,11 @@ async function main(): Promise<void> {
log(false, 'CREATOR-CLI', '❌ ACCOUNT CREATION FAILED', 'error') log(false, 'CREATOR-CLI', '❌ ACCOUNT CREATION FAILED', 'error')
log(false, 'CREATOR-CLI', '='.repeat(60), 'error') log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line
await browserContext.close() await browserContext.close()
process.exit(1) process.exit(1)
} }
} catch (error) { } catch (error) {
const msg = error instanceof Error ? error.message : String(error) const msg = error instanceof Error ? error.message : String(error)
log(false, 'CREATOR-CLI', '', 'log') // Empty line log(false, 'CREATOR-CLI', '', 'log') // Empty line

View File

@@ -4,8 +4,8 @@ import playwright, { BrowserContext } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../interface/Account'
import { loadSessionData, saveFingerprintData } from '../util/Load' import { updateFingerprintUserAgent } from '../util/browser/UserAgent'
import { updateFingerprintUserAgent } from '../util/UserAgent' import { loadSessionData, saveFingerprintData } from '../util/state/Load'
class Browser { class Browser {
private bot: MicrosoftRewardsBot private bot: MicrosoftRewardsBot
@@ -22,7 +22,7 @@ class Browser {
this.bot.log(this.bot.isMobile, 'BROWSER', 'Auto-installing Chromium...', 'log') this.bot.log(this.bot.isMobile, 'BROWSER', 'Auto-installing Chromium...', 'log')
execSync('npx playwright install chromium', { stdio: 'ignore', timeout: 120000 }) execSync('npx playwright install chromium', { stdio: 'ignore', timeout: 120000 })
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium installed successfully', 'log') this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium installed successfully', 'log')
} catch (e) { } catch (e) {
// FIXED: Improved error logging (no longer silent) // FIXED: Improved error logging (no longer silent)
const errorMsg = e instanceof Error ? e.message : String(e) const errorMsg = e instanceof Error ? e.message : String(e)
this.bot.log(this.bot.isMobile, 'BROWSER', `Auto-install failed: ${errorMsg}`, 'warn') this.bot.log(this.bot.isMobile, 'BROWSER', `Auto-install failed: ${errorMsg}`, 'warn')
@@ -33,13 +33,13 @@ class Browser {
try { try {
const envForceHeadless = process.env.FORCE_HEADLESS === '1' const envForceHeadless = process.env.FORCE_HEADLESS === '1'
const headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false) const headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false)
const engineName = 'chromium' const engineName = 'chromium'
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`)
const proxyConfig = this.buildPlaywrightProxy(proxy) const proxyConfig = this.buildPlaywrightProxy(proxy)
const isLinux = process.platform === 'linux' const isLinux = process.platform === 'linux'
// Base arguments for stability // Base arguments for stability
const baseArgs = [ const baseArgs = [
'--no-sandbox', '--no-sandbox',
@@ -49,7 +49,7 @@ class Browser {
'--ignore-certificate-errors-spki-list', '--ignore-certificate-errors-spki-list',
'--ignore-ssl-errors' '--ignore-ssl-errors'
] ]
// Linux stability fixes // Linux stability fixes
const linuxStabilityArgs = isLinux ? [ const linuxStabilityArgs = isLinux ? [
'--disable-dev-shm-usage', '--disable-dev-shm-usage',
@@ -88,10 +88,10 @@ class Browser {
try { try {
context.on('page', async (page) => { context.on('page', async (page) => {
try { try {
const viewport = this.bot.isMobile const viewport = this.bot.isMobile
? { width: 390, height: 844 } ? { width: 390, height: 844 }
: { width: 1280, height: 800 } : { width: 1280, height: 800 }
await page.setViewportSize(viewport) await page.setViewportSize(viewport)
// Standard styling // Standard styling
@@ -106,13 +106,13 @@ class Browser {
} }
` `
document.documentElement.appendChild(style) document.documentElement.appendChild(style)
} catch {/* ignore */} } catch {/* ignore */ }
}) })
} catch (e) { } catch (e) {
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
} }
}) })
} catch (e) { } catch (e) {
this.bot.log(this.bot.isMobile, 'BROWSER', `Context event handler warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') this.bot.log(this.bot.isMobile, 'BROWSER', `Context event handler warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
} }

View File

@@ -8,7 +8,7 @@ import { AppUserData } from '../interface/AppUserData'
import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData' import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
import { EarnablePoints } from '../interface/Points' import { EarnablePoints } from '../interface/Points'
import { QuizData } from '../interface/QuizData' import { QuizData } from '../interface/QuizData'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/state/Load'
export default class BrowserFunc { export default class BrowserFunc {
@@ -29,12 +29,12 @@ export default class BrowserFunc {
const suspendedByHeader = await page.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 }) const suspendedByHeader = await page.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 })
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false)
if (suspendedByHeader) { if (suspendedByHeader) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error') this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error')
return true return true
} }
// Secondary check: look for suspension text in main content area only // Secondary check: look for suspension text in main content area only
try { try {
const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || '' const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || ''
@@ -43,7 +43,7 @@ export default class BrowserFunc {
/suspended\s+due\s+to\s+unusual\s+activity/i, /suspended\s+due\s+to\s+unusual\s+activity/i,
/your\s+account\s+is\s+temporarily\s+suspended/i /your\s+account\s+is\s+temporarily\s+suspended/i
] ]
const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent)) const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent))
if (isSuspended) { if (isSuspended) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error') this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
@@ -54,7 +54,7 @@ export default class BrowserFunc {
const errorMsg = error instanceof Error ? error.message : String(error) const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn') this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn')
} }
return false return false
} }
@@ -90,7 +90,7 @@ export default class BrowserFunc {
if (isSuspended) { if (isSuspended) {
throw new Error('Account has been suspended!') throw new Error('Account has been suspended!')
} }
// Not suspended, just activities not loaded yet - continue to next iteration // Not suspended, just activities not loaded yet - continue to next iteration
this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn') this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn')
} }
@@ -133,10 +133,10 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page') this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
await this.goHome(target) await this.goHome(target)
} }
// Reload with retry // Reload with retry
await this.reloadPageWithRetry(target, 2) await this.reloadPageWithRetry(target, 2)
// Wait for the more-activities element to ensure page is fully loaded // Wait for the more-activities element to ensure page is fully loaded
await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((error) => { await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((error) => {
// Continuing is intentional: page may still be functional even if this specific element is missing // Continuing is intentional: page may still be functional even if this specific element is missing
@@ -149,7 +149,7 @@ export default class BrowserFunc {
if (!scriptContent) { if (!scriptContent) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn') this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn')
// Force a navigation retry once before failing hard // Force a navigation retry once before failing hard
await this.goHome(target) await this.goHome(target)
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch((error) => { await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch((error) => {
@@ -157,9 +157,9 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load failed: ${errorMsg}`, 'warn') this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load failed: ${errorMsg}`, 'warn')
}) })
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM) await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
scriptContent = await this.extractDashboardScript(target) scriptContent = await this.extractDashboardScript(target)
if (!scriptContent) { if (!scriptContent) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error') this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
throw new Error('Dashboard data not found within script - check page structure') throw new Error('Dashboard data not found within script - check page structure')
@@ -192,14 +192,14 @@ export default class BrowserFunc {
const startTime = Date.now() const startTime = Date.now()
const MAX_TOTAL_TIME_MS = 30000 // 30 seconds max total const MAX_TOTAL_TIME_MS = 30000 // 30 seconds max total
let lastError: unknown = null let lastError: unknown = null
for (let attempt = 1; attempt <= maxAttempts; attempt++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
// Check global timeout // Check global timeout
if (Date.now() - startTime > MAX_TOTAL_TIME_MS) { if (Date.now() - startTime > MAX_TOTAL_TIME_MS) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload retry exceeded total timeout (${MAX_TOTAL_TIME_MS}ms)`, 'warn') this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload retry exceeded total timeout (${MAX_TOTAL_TIME_MS}ms)`, 'warn')
break break
} }
try { try {
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload({ waitUntil: 'domcontentloaded' })
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM) await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
@@ -212,7 +212,7 @@ export default class BrowserFunc {
if (msg.includes('has been closed')) { if (msg.includes('has been closed')) {
if (attempt === 1) { if (attempt === 1) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn') this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
try { await this.goHome(page) } catch {/* ignore */} try { await this.goHome(page) } catch {/* ignore */ }
} else { } else {
break break
} }
@@ -222,7 +222,7 @@ export default class BrowserFunc {
} }
} }
} }
if (lastError) throw lastError if (lastError) throw lastError
} }
@@ -233,12 +233,12 @@ export default class BrowserFunc {
return await page.evaluate(() => { return await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script')) const scripts = Array.from(document.querySelectorAll('script'))
const dashboardPatterns = ['var dashboard', 'dashboard=', 'dashboard :'] const dashboardPatterns = ['var dashboard', 'dashboard=', 'dashboard :']
const targetScript = scripts.find(script => { const targetScript = scripts.find(script => {
const text = script.innerText const text = script.innerText
return text && dashboardPatterns.some(pattern => text.includes(pattern)) return text && dashboardPatterns.some(pattern => text.includes(pattern))
}) })
return targetScript?.innerText || null return targetScript?.innerText || null
}) })
} }
@@ -265,19 +265,19 @@ export default class BrowserFunc {
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) { if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
continue continue
} }
const parsed = JSON.parse(jsonStr) const parsed = JSON.parse(jsonStr)
// Enhanced validation: check structure and type // Enhanced validation: check structure and type
if (typeof parsed !== 'object' || parsed === null) { if (typeof parsed !== 'object' || parsed === null) {
continue continue
} }
// Validate essential dashboard properties exist // Validate essential dashboard properties exist
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') { if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
continue continue
} }
// Successfully validated dashboard structure // Successfully validated dashboard structure
return parsed return parsed
} catch (e) { } catch (e) {
@@ -401,7 +401,7 @@ export default class BrowserFunc {
const checkInDay = parseInt(item.attributes.progress ?? '', 10) % 7 const checkInDay = parseInt(item.attributes.progress ?? '', 10) % 7
const today = new Date() const today = new Date()
const lastUpdated = new Date(item.attributes.last_updated ?? '') const lastUpdated = new Date(item.attributes.last_updated ?? '')
if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) { if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) {
points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '', 10) points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '', 10)
} }
@@ -493,10 +493,10 @@ export default class BrowserFunc {
.map(el => $(el).text()) .map(el => $(el).text())
.filter(t => t.length > 0) .filter(t => t.length > 0)
.map(t => t.substring(0, 100)) .map(t => t.substring(0, 100))
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error')
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn')
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error') this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
throw new Error('Script containing quiz data not found - check page structure') throw new Error('Script containing quiz data not found - check page structure')
} }
@@ -545,10 +545,10 @@ export default class BrowserFunc {
const html = await page.content() const html = await page.content()
const $ = load(html) const $ = load(html)
const element = $('.offer-cta').toArray().find((x: unknown) => { const element = $('.offer-cta').toArray().find((x: unknown) => {
const el = x as { attribs?: { href?: string } } const el = x as { attribs?: { href?: string } }
return !!el.attribs?.href?.includes(activity.offerId) return !!el.attribs?.href?.includes(activity.offerId)
}) })
if (element) { if (element) {
selector = `a[href*="${element.attribs.href}"]` selector = `a[href*="${element.attribs.href}"]`
} }

View File

@@ -1,7 +1,7 @@
import { load } from 'cheerio' import { load } from 'cheerio'
import { Page } from 'rebrowser-playwright' import { Page } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { logError } from '../util/Logger' import { logError } from '../util/notifications/Logger'
type DismissButton = { selector: string; label: string; isXPath?: boolean } type DismissButton = { selector: string; label: string; isXPath?: boolean }
@@ -145,14 +145,14 @@ export default class BrowserUtil {
private async dismissTermsUpdateDialog(page: Page): Promise<number> { private async dismissTermsUpdateDialog(page: Page): Promise<number> {
try { try {
const { titleId, titleText, nextButton } = BrowserUtil.TERMS_UPDATE_SELECTORS const { titleId, titleText, nextButton } = BrowserUtil.TERMS_UPDATE_SELECTORS
// Check if terms update page is present // Check if terms update page is present
const titleById = page.locator(titleId) const titleById = page.locator(titleId)
const titleByText = page.locator('h1').filter({ hasText: titleText }) const titleByText = page.locator('h1').filter({ hasText: titleText })
const hasTitle = await titleById.isVisible({ timeout: 200 }).catch(() => false) || const hasTitle = await titleById.isVisible({ timeout: 200 }).catch(() => false) ||
await titleByText.first().isVisible({ timeout: 200 }).catch(() => false) await titleByText.first().isVisible({ timeout: 200 }).catch(() => false)
if (!hasTitle) return 0 if (!hasTitle) return 0
// Click the Next button // Click the Next button
@@ -199,9 +199,9 @@ export default class BrowserUtil {
const $ = load(html) const $ = load(html)
const isNetworkError = $('body.neterror').length const isNetworkError = $('body.neterror').length
const hasHttp400Error = html.includes('HTTP ERROR 400') || const hasHttp400Error = html.includes('HTTP ERROR 400') ||
html.includes('This page isn\'t working') || html.includes('This page isn\'t working') ||
html.includes('This page is not working') html.includes('This page is not working')
if (isNetworkError || hasHttp400Error) { if (isNetworkError || hasHttp400Error) {
const errorType = hasHttp400Error ? 'HTTP 400' : 'network error' const errorType = hasHttp400Error ? 'HTTP 400' : 'network error'

View File

@@ -18,7 +18,7 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
const parsed = Number(raw) const parsed = Number(raw)
if (!Number.isFinite(parsed)) { if (!Number.isFinite(parsed)) {
queueMicrotask(() => { queueMicrotask(() => {
import('./util/Logger').then(({ log }) => { import('./util/notifications/Logger').then(({ log }) => {
log('main', 'CONSTANTS', `Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`, 'warn') log('main', 'CONSTANTS', `Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`, 'warn')
}).catch(() => { }).catch(() => {
process.stderr.write(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}\n`) process.stderr.write(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}\n`)
@@ -29,7 +29,7 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
if (parsed < min || parsed > max) { if (parsed < min || parsed > max) {
queueMicrotask(() => { queueMicrotask(() => {
import('./util/Logger').then(({ log }) => { import('./util/notifications/Logger').then(({ log }) => {
log('main', 'CONSTANTS', `${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`, 'warn') log('main', 'CONSTANTS', `${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`, 'warn')
}).catch(() => { }).catch(() => {
process.stderr.write(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}\n`) process.stderr.write(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}\n`)

View File

@@ -1,6 +1,6 @@
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import { log as botLog } from '../util/Logger' import { getErrorMessage } from '../util/core/Utils'
import { getErrorMessage } from '../util/Utils' import { log as botLog } from '../util/notifications/Logger'
import { dashboardState } from './state' import { dashboardState } from './state'
export class BotController { export class BotController {
@@ -14,7 +14,7 @@ export class BotController {
private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void { private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
botLog('main', 'BOT-CONTROLLER', message, level) botLog('main', 'BOT-CONTROLLER', message, level)
dashboardState.addLog({ dashboardState.addLog({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
level, level,
@@ -29,7 +29,7 @@ export class BotController {
if (this.botInstance) { if (this.botInstance) {
return { success: false, error: 'Bot is already running' } return { success: false, error: 'Bot is already running' }
} }
if (this.isStarting) { if (this.isStarting) {
return { success: false, error: 'Bot is currently starting, please wait' } return { success: false, error: 'Bot is currently starting, please wait' }
} }
@@ -39,7 +39,7 @@ export class BotController {
this.log('🚀 Starting bot...', 'log') this.log('🚀 Starting bot...', 'log')
const { MicrosoftRewardsBot } = await import('../index') const { MicrosoftRewardsBot } = await import('../index')
this.botInstance = new MicrosoftRewardsBot(false) this.botInstance = new MicrosoftRewardsBot(false)
this.startTime = new Date() this.startTime = new Date()
dashboardState.setRunning(true) dashboardState.setRunning(true)
@@ -49,10 +49,10 @@ export class BotController {
void (async () => { void (async () => {
try { try {
this.log('✓ Bot initialized, starting execution...', 'log') this.log('✓ Bot initialized, starting execution...', 'log')
await this.botInstance!.initialize() await this.botInstance!.initialize()
await this.botInstance!.run() await this.botInstance!.run()
this.log('✓ Bot completed successfully', 'log') this.log('✓ Bot completed successfully', 'log')
} catch (error) { } catch (error) {
this.log(`Bot error: ${getErrorMessage(error)}`, 'error') this.log(`Bot error: ${getErrorMessage(error)}`, 'error')
@@ -81,7 +81,7 @@ export class BotController {
try { try {
this.log('🛑 Stopping bot...', 'warn') this.log('🛑 Stopping bot...', 'warn')
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()
return { success: true } return { success: true }
@@ -95,14 +95,14 @@ export class BotController {
public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> { public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> {
this.log('🔄 Restarting bot...', 'log') this.log('🔄 Restarting bot...', 'log')
const stopResult = this.stop() const stopResult = this.stop()
if (!stopResult.success && stopResult.error !== 'Bot is not running') { if (!stopResult.success && stopResult.error !== 'Bot is not running') {
return { success: false, error: `Failed to stop: ${stopResult.error}` } return { success: false, error: `Failed to stop: ${stopResult.error}` }
} }
await this.wait(2000) await this.wait(2000)
return await this.start() return await this.start()
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express' import { Request, Response, Router } from 'express'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { getConfigPath, loadAccounts, loadConfig } from '../util/Load' import { getConfigPath, loadAccounts, loadConfig } from '../util/state/Load'
import { botController } from './BotController' import { botController } from './BotController'
import { dashboardState } from './state' import { dashboardState } from './state'
@@ -100,7 +100,7 @@ apiRouter.get('/config', (_req: Request, res: Response) => {
try { try {
const config = loadConfig() const config = loadConfig()
const safe = JSON.parse(JSON.stringify(config)) const safe = JSON.parse(JSON.stringify(config))
// Mask sensitive data // Mask sensitive data
if (safe.webhook?.url) safe.webhook.url = maskUrl(safe.webhook.url) if (safe.webhook?.url) safe.webhook.url = maskUrl(safe.webhook.url)
if (safe.conclusionWebhook?.url) safe.conclusionWebhook.url = maskUrl(safe.conclusionWebhook.url) if (safe.conclusionWebhook?.url) safe.conclusionWebhook.url = maskUrl(safe.conclusionWebhook.url)
@@ -117,7 +117,7 @@ apiRouter.post('/config', (req: Request, res: Response): void => {
try { try {
const newConfig = req.body const newConfig = req.body
const configPath = getConfigPath() const configPath = getConfigPath()
if (!configPath || !fs.existsSync(configPath)) { if (!configPath || !fs.existsSync(configPath)) {
res.status(404).json({ error: 'Config file not found' }) res.status(404).json({ error: 'Config file not found' })
return return
@@ -146,7 +146,7 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> =>
} }
const result = await botController.start() const result = await botController.start()
if (result.success) { if (result.success) {
sendSuccess(res, { message: 'Bot started successfully', pid: result.pid }) sendSuccess(res, { message: 'Bot started successfully', pid: result.pid })
} else { } else {
@@ -161,7 +161,7 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> =>
apiRouter.post('/stop', (_req: Request, res: Response): void => { apiRouter.post('/stop', (_req: Request, res: Response): void => {
try { try {
const result = botController.stop() const result = botController.stop()
if (result.success) { if (result.success) {
sendSuccess(res, { message: 'Bot stopped successfully' }) sendSuccess(res, { message: 'Bot stopped successfully' })
} else { } else {
@@ -176,7 +176,7 @@ apiRouter.post('/stop', (_req: Request, res: Response): void => {
apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> => { apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> => {
try { try {
const result = await botController.restart() const result = await botController.restart()
if (result.success) { if (result.success) {
sendSuccess(res, { message: 'Bot restarted successfully', pid: result.pid }) sendSuccess(res, { message: 'Bot restarted successfully', pid: result.pid })
} else { } else {
@@ -194,7 +194,7 @@ apiRouter.get('/metrics', (_req: Request, res: Response) => {
const totalPoints = accounts.reduce((sum, a) => sum + (a.points || 0), 0) const totalPoints = accounts.reduce((sum, a) => sum + (a.points || 0), 0)
const accountsWithErrors = accounts.filter(a => a.errors && a.errors.length > 0).length const accountsWithErrors = accounts.filter(a => a.errors && a.errors.length > 0).length
const avgPoints = accounts.length > 0 ? Math.round(totalPoints / accounts.length) : 0 const avgPoints = accounts.length > 0 ? Math.round(totalPoints / accounts.length) : 0
res.json({ res.json({
totalAccounts: accounts.length, totalAccounts: accounts.length,
totalPoints, totalPoints,
@@ -218,14 +218,14 @@ apiRouter.get('/account/:email', (req: Request, res: Response): void => {
res.status(400).json({ error: 'Email parameter required' }) res.status(400).json({ error: 'Email parameter required' })
return return
} }
const account = dashboardState.getAccount(email) const account = dashboardState.getAccount(email)
if (!account) { if (!account) {
res.status(404).json({ error: 'Account not found' }) res.status(404).json({ error: 'Account not found' })
return return
} }
res.json(account) res.json(account)
} catch (error) { } catch (error) {
res.status(500).json({ error: getErr(error) }) res.status(500).json({ error: getErr(error) })
@@ -240,19 +240,19 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
res.status(400).json({ error: 'Email parameter required' }) res.status(400).json({ error: 'Email parameter required' })
return return
} }
const account = dashboardState.getAccount(email) const account = dashboardState.getAccount(email)
if (!account) { if (!account) {
res.status(404).json({ error: 'Account not found' }) res.status(404).json({ error: 'Account not found' })
return return
} }
dashboardState.updateAccount(email, { dashboardState.updateAccount(email, {
status: 'idle', status: 'idle',
errors: [] errors: []
}) })
res.json({ success: true }) res.json({ success: true })
} catch (error) { } catch (error) {
res.status(500).json({ error: getErr(error) }) res.status(500).json({ error: getErr(error) })
@@ -263,10 +263,10 @@ 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)
const maskedHost = parsed.hostname.length > 6 const maskedHost = parsed.hostname.length > 6
? `${parsed.hostname.slice(0, 3)}***${parsed.hostname.slice(-3)}` ? `${parsed.hostname.slice(0, 3)}***${parsed.hostname.slice(-3)}`
: '***' : '***'
const maskedPath = parsed.pathname.length > 5 const maskedPath = parsed.pathname.length > 5
? `${parsed.pathname.slice(0, 3)}***` ? `${parsed.pathname.slice(0, 3)}***`
: '***' : '***'
return `${parsed.protocol}//${maskedHost}${maskedPath}` return `${parsed.protocol}//${maskedHost}${maskedPath}`

View File

@@ -3,7 +3,7 @@ import fs from 'fs'
import { createServer } from 'http' import { createServer } from 'http'
import path from 'path' import path from 'path'
import { WebSocket, WebSocketServer } from 'ws' import { WebSocket, WebSocketServer } from 'ws'
import { log as botLog } from '../util/Logger' import { log as botLog } from '../util/notifications/Logger'
import { apiRouter } from './routes' import { apiRouter } from './routes'
import { DashboardLog, dashboardState } from './state' import { DashboardLog, dashboardState } from './state'
@@ -41,7 +41,7 @@ export class DashboardServer {
private setupMiddleware(): void { private setupMiddleware(): void {
this.app.use(express.json()) this.app.use(express.json())
// Disable caching for all static files // Disable caching for all static files
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private') res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
@@ -49,7 +49,7 @@ export class DashboardServer {
res.set('Expires', '0') res.set('Expires', '0')
next() next()
}) })
this.app.use('/assets', express.static(path.join(__dirname, '../../assets'), { this.app.use('/assets', express.static(path.join(__dirname, '../../assets'), {
etag: false, etag: false,
maxAge: 0 maxAge: 0
@@ -62,7 +62,7 @@ export class DashboardServer {
private setupRoutes(): void { private setupRoutes(): void {
this.app.use('/api', apiRouter) this.app.use('/api', apiRouter)
// Health check // Health check
this.app.get('/health', (_req, res) => { this.app.get('/health', (_req, res) => {
res.json({ status: 'ok', uptime: process.uptime() }) res.json({ status: 'ok', uptime: process.uptime() })
@@ -71,12 +71,12 @@ export class DashboardServer {
// Serve dashboard UI // Serve dashboard UI
this.app.get('/', (_req, res) => { this.app.get('/', (_req, res) => {
const indexPath = path.join(__dirname, '../../public/index.html') const indexPath = path.join(__dirname, '../../public/index.html')
// Force no cache on HTML files // Force no cache on HTML files
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private') res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
res.set('Pragma', 'no-cache') res.set('Pragma', 'no-cache')
res.set('Expires', '0') res.set('Expires', '0')
if (fs.existsSync(indexPath)) { if (fs.existsSync(indexPath)) {
res.sendFile(indexPath) res.sendFile(indexPath)
} else { } else {
@@ -117,9 +117,9 @@ export class DashboardServer {
const recentLogs = dashboardState.getLogs(100) const recentLogs = dashboardState.getLogs(100)
const status = dashboardState.getStatus() const status = dashboardState.getStatus()
const accounts = dashboardState.getAccounts() const accounts = dashboardState.getAccounts()
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'init', type: 'init',
data: { data: {
logs: recentLogs, logs: recentLogs,
status, status,
@@ -135,7 +135,7 @@ export class DashboardServer {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const loggerModule = require('../util/Logger') as { log: typeof botLog } const loggerModule = require('../util/Logger') as { log: typeof botLog }
const originalLog = loggerModule.log const originalLog = loggerModule.log
loggerModule.log = ( loggerModule.log = (
isMobile: boolean | 'main', isMobile: boolean | 'main',
title: string, title: string,
@@ -145,7 +145,7 @@ export class DashboardServer {
) => { ) => {
// Call original log function // Call original log function
const result = originalLog(isMobile, title, message, type, color as keyof typeof import('chalk')) const result = originalLog(isMobile, title, message, type, color as keyof typeof import('chalk'))
// Create log entry for dashboard // Create log entry for dashboard
const logEntry: DashboardLog = { const logEntry: DashboardLog = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -154,14 +154,14 @@ export class DashboardServer {
title, title,
message message
} }
// Add to dashboard state and broadcast // Add to dashboard state and broadcast
dashboardState.addLog(logEntry) dashboardState.addLog(logEntry)
this.broadcastUpdate('log', { log: logEntry }) this.broadcastUpdate('log', { log: logEntry })
return result return result
} }
dashLog('Bot log interception active') dashLog('Bot log interception active')
} }

View File

@@ -12,7 +12,7 @@
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import type { Account } from '../interface/Account' import type { Account } from '../interface/Account'
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory' import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
import { handleCompromisedMode } from './FlowUtils' import { handleCompromisedMode } from './FlowUtils'
export interface DesktopFlowResult { export interface DesktopFlowResult {

View File

@@ -4,7 +4,7 @@
*/ */
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/state/Load'
/** /**
* Handle compromised/security check mode for an account * Handle compromised/security check mode for an account
@@ -27,7 +27,7 @@ export async function handleCompromisedMode(
isMobile: boolean isMobile: boolean
): Promise<{ keepBrowserOpen: boolean }> { ): Promise<{ keepBrowserOpen: boolean }> {
const flowContext = isMobile ? 'MOBILE-FLOW' : 'DESKTOP-FLOW' const flowContext = isMobile ? 'MOBILE-FLOW' : 'DESKTOP-FLOW'
bot.log( bot.log(
isMobile, isMobile,
flowContext, flowContext,
@@ -35,10 +35,10 @@ export async function handleCompromisedMode(
'warn', 'warn',
'yellow' 'yellow'
) )
// Send security alert webhook // Send security alert webhook
try { try {
const { ConclusionWebhook } = await import('../util/ConclusionWebhook') const { ConclusionWebhook } = await import('../util/notifications/ConclusionWebhook')
await ConclusionWebhook( await ConclusionWebhook(
bot.config, bot.config,
isMobile ? '🔐 Security Check (Mobile)' : '🔐 Security Check', isMobile ? '🔐 Security Check (Mobile)' : '🔐 Security Check',
@@ -50,7 +50,7 @@ export async function handleCompromisedMode(
const errorMsg = error instanceof Error ? error.message : String(error) const errorMsg = error instanceof Error ? error.message : String(error)
bot.log(isMobile, flowContext, `Failed to send security webhook: ${errorMsg}`, 'warn') bot.log(isMobile, flowContext, `Failed to send security webhook: ${errorMsg}`, 'warn')
} }
// Save session for convenience (non-critical) // Save session for convenience (non-critical)
try { try {
await saveSessionData(bot.config.sessionPath, bot.homePage.context(), account, isMobile) await saveSessionData(bot.config.sessionPath, bot.homePage.context(), account, isMobile)
@@ -58,6 +58,6 @@ export async function handleCompromisedMode(
const errorMsg = error instanceof Error ? error.message : String(error) const errorMsg = error instanceof Error ? error.message : String(error)
bot.log(isMobile, flowContext, `Failed to save session: ${errorMsg}`, 'warn') bot.log(isMobile, flowContext, `Failed to save session: ${errorMsg}`, 'warn')
} }
return { keepBrowserOpen: true } return { keepBrowserOpen: true }
} }

View File

@@ -13,8 +13,8 @@
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import type { Account } from '../interface/Account' import type { Account } from '../interface/Account'
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory' import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
import { MobileRetryTracker } from '../util/MobileRetryTracker' import { MobileRetryTracker } from '../util/state/MobileRetryTracker'
import { handleCompromisedMode } from './FlowUtils' import { handleCompromisedMode } from './FlowUtils'
export interface MobileFlowResult { export interface MobileFlowResult {

View File

@@ -10,10 +10,10 @@
*/ */
import type { Config } from '../interface/Config' import type { Config } from '../interface/Config'
import { ConclusionWebhook } from '../util/ConclusionWebhook' import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
import { JobState } from '../util/JobState' import { log } from '../util/notifications/Logger'
import { log } from '../util/Logger' import { Ntfy } from '../util/notifications/Ntfy'
import { Ntfy } from '../util/Ntfy' import { JobState } from '../util/state/JobState'
export interface AccountResult { export interface AccountResult {
email: string email: string
@@ -54,7 +54,7 @@ export class SummaryReporter {
const minutes = Math.floor((duration % 3600) / 60) const minutes = Math.floor((duration % 3600) / 60)
const seconds = duration % 60 const seconds = duration % 60
const durationText = hours > 0 const durationText = hours > 0
? `${hours}h ${minutes}m ${seconds}s` ? `${hours}h ${minutes}m ${seconds}s`
: minutes > 0 : minutes > 0
? `${minutes}m ${seconds}s` ? `${minutes}m ${seconds}s`
@@ -67,7 +67,7 @@ export class SummaryReporter {
for (const account of summary.accounts) { for (const account of summary.accounts) {
const status = account.errors?.length ? '❌' : '✅' const status = account.errors?.length ? '❌' : '✅'
description += `${status} ${account.email}: ${account.pointsEarned} points (${Math.round(account.runDuration / 1000)}s)\n` description += `${status} ${account.email}: ${account.pointsEarned} points (${Math.round(account.runDuration / 1000)}s)\n`
if (account.errors?.length) { if (account.errors?.length) {
description += ` ⚠️ ${account.errors[0]}\n` description += ` ⚠️ ${account.errors[0]}\n`
} }
@@ -95,7 +95,7 @@ export class SummaryReporter {
try { try {
const message = `Collected ${summary.totalPoints} points across ${summary.accounts.length} account(s). Success: ${summary.successCount}, Failed: ${summary.failureCount}` const message = `Collected ${summary.totalPoints} points across ${summary.accounts.length} account(s). Success: ${summary.successCount}, Failed: ${summary.failureCount}`
await Ntfy(message, summary.failureCount > 0 ? 'warn' : 'log') await Ntfy(message, summary.failureCount > 0 ? 'warn' : 'log')
} catch (error) { } catch (error) {
log('main', 'SUMMARY', `Failed to send Ntfy notification: ${error instanceof Error ? error.message : String(error)}`, 'error') log('main', 'SUMMARY', `Failed to send Ntfy notification: ${error instanceof Error ? error.message : String(error)}`, 'error')
@@ -109,7 +109,7 @@ export class SummaryReporter {
try { try {
const day = summary.endTime.toISOString().split('T')?.[0] const day = summary.endTime.toISOString().split('T')?.[0]
if (!day) return if (!day) return
for (const account of summary.accounts) { for (const account of summary.accounts) {
this.jobState.markAccountComplete( this.jobState.markAccountComplete(
account.email, account.email,
@@ -133,12 +133,12 @@ export class SummaryReporter {
log('main', 'SUMMARY', '═'.repeat(80)) log('main', 'SUMMARY', '═'.repeat(80))
log('main', 'SUMMARY', '📊 EXECUTION SUMMARY') log('main', 'SUMMARY', '📊 EXECUTION SUMMARY')
log('main', 'SUMMARY', '═'.repeat(80)) log('main', 'SUMMARY', '═'.repeat(80))
const duration = Math.round((summary.endTime.getTime() - summary.startTime.getTime()) / 1000) const duration = Math.round((summary.endTime.getTime() - summary.startTime.getTime()) / 1000)
log('main', 'SUMMARY', `⏱️ Duration: ${Math.floor(duration / 60)}m ${duration % 60}s`) log('main', 'SUMMARY', `⏱️ Duration: ${Math.floor(duration / 60)}m ${duration % 60}s`)
log('main', 'SUMMARY', `📈 Total Points Collected: ${summary.totalPoints}`) log('main', 'SUMMARY', `📈 Total Points Collected: ${summary.totalPoints}`)
log('main', 'SUMMARY', `✅ Successful Accounts: ${summary.successCount}/${summary.accounts.length}`) log('main', 'SUMMARY', `✅ Successful Accounts: ${summary.successCount}/${summary.accounts.length}`)
if (summary.failureCount > 0) { if (summary.failureCount > 0) {
log('main', 'SUMMARY', `❌ Failed Accounts: ${summary.failureCount}`, 'warn') log('main', 'SUMMARY', `❌ Failed Accounts: ${summary.failureCount}`, 'warn')
} }
@@ -150,10 +150,10 @@ export class SummaryReporter {
for (const account of summary.accounts) { for (const account of summary.accounts) {
const status = account.errors?.length ? '❌ FAILED' : '✅ SUCCESS' const status = account.errors?.length ? '❌ 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.errors?.length) {
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error') log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,23 +4,23 @@ import { TIMEOUTS } from '../constants'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData' import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { AdaptiveThrottler } from '../util/AdaptiveThrottler' import { Retry } from '../util/core/Retry'
import JobState from '../util/JobState' import { AdaptiveThrottler } from '../util/notifications/AdaptiveThrottler'
import { logError } from '../util/Logger' import { logError } from '../util/notifications/Logger'
import { Retry } from '../util/Retry' import JobState from '../util/state/JobState'
// Selector patterns (extracted to avoid magic strings) // Selector patterns (extracted to avoid magic strings)
const ACTIVITY_SELECTORS = { const ACTIVITY_SELECTORS = {
byName: (name: string) => `[data-bi-id^="${name}"] .pointLink:not(.contentContainer .pointLink)`, byName: (name: string) => `[data-bi-id^="${name}"] .pointLink:not(.contentContainer .pointLink)`,
byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .pointLink:not(.contentContainer .pointLink)` byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .pointLink:not(.contentContainer .pointLink)`
} as const } as const
// Activity processing delays (in milliseconds) // Activity processing delays (in milliseconds)
const ACTIVITY_DELAYS = { const ACTIVITY_DELAYS = {
THROTTLE_MIN: 800, THROTTLE_MIN: 800,
THROTTLE_MAX: 1400, THROTTLE_MAX: 1400,
ACTIVITY_SPACING_MIN: 1200, ACTIVITY_SPACING_MIN: 1200,
ACTIVITY_SPACING_MAX: 2600 ACTIVITY_SPACING_MAX: 2600
} as const } as const
export class Workers { export class Workers {
@@ -220,9 +220,9 @@ export class Workers {
if (!activity.offerId) { if (!activity.offerId) {
// IMPROVED: More prominent logging for data integrity issue // IMPROVED: More prominent logging for data integrity issue
this.bot.log( this.bot.log(
this.bot.isMobile, this.bot.isMobile,
'WORKERS', 'WORKERS',
`⚠️ DATA INTEGRITY: Activity "${activity.name || activity.title}" is missing offerId field. This may indicate a dashboard API change or data corruption. Falling back to name-based selector.`, `⚠️ DATA INTEGRITY: Activity "${activity.name || activity.title}" is missing offerId field. This may indicate a dashboard API change or data corruption. Falling back to name-based selector.`,
'warn' 'warn'
) )
return ACTIVITY_SELECTORS.byName(activity.name) return ACTIVITY_SELECTORS.byName(activity.name)
@@ -239,7 +239,7 @@ export class Workers {
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> { private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
// Check if element exists before clicking (avoid 30s timeout) // Check if element exists before clicking (avoid 30s timeout)
try { try {
await page.waitForSelector(selector, { timeout: TIMEOUTS.NETWORK_IDLE }) await page.waitForSelector(selector, { timeout: TIMEOUTS.NETWORK_IDLE })
@@ -254,7 +254,7 @@ export class Workers {
// Execute activity with timeout protection using Promise.race // Execute activity with timeout protection using Promise.race
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2 const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
await retry.run(async () => { await retry.run(async () => {
const activityPromise = this.bot.activities.run(page, activity) const activityPromise = this.bot.activities.run(page, activity)
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
@@ -264,7 +264,7 @@ export class Workers {
// Clean up timer if activity completes first // Clean up timer if activity completes first
activityPromise.finally(() => clearTimeout(timer)) activityPromise.finally(() => clearTimeout(timer))
}) })
try { try {
await Promise.race([activityPromise, timeoutPromise]) await Promise.race([activityPromise, timeoutPromise])
throttle.record(true) throttle.record(true)

View File

@@ -7,16 +7,16 @@ import type { Page } from 'playwright'
import { createInterface } from 'readline' import { createInterface } from 'readline'
import BrowserFunc from './browser/BrowserFunc' import BrowserFunc from './browser/BrowserFunc'
import BrowserUtil from './browser/BrowserUtil' import BrowserUtil from './browser/BrowserUtil'
import Axios from './util/Axios' import Humanizer from './util/browser/Humanizer'
import { detectBanReason } from './util/BanDetector' import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils'
import Humanizer from './util/Humanizer' import Axios from './util/network/Axios'
import JobState from './util/JobState' import { QueryDiversityEngine } from './util/network/QueryDiversityEngine'
import { loadAccounts, loadConfig } from './util/Load' import { log } from './util/notifications/Logger'
import { log } from './util/Logger' import JobState from './util/state/JobState'
import { MobileRetryTracker } from './util/MobileRetryTracker' import { loadAccounts, loadConfig } from './util/state/Load'
import { QueryDiversityEngine } from './util/QueryDiversityEngine' import { MobileRetryTracker } from './util/state/MobileRetryTracker'
import { StartupValidator } from './util/StartupValidator' import { detectBanReason } from './util/validation/BanDetector'
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/Utils' import { StartupValidator } from './util/validation/StartupValidator'
import { Activities } from './functions/Activities' import { Activities } from './functions/Activities'
import { Login } from './functions/Login' import { Login } from './functions/Login'
@@ -629,7 +629,7 @@ export class MicrosoftRewardsBot {
try { try {
const h = this.config?.humanization const h = this.config?.humanization
if (!h || h.immediateBanAlert === false) return if (!h || h.immediateBanAlert === false) return
const { ConclusionWebhook } = await import('./util/ConclusionWebhook') const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
await ConclusionWebhook( await ConclusionWebhook(
this.config, this.config,
'🚫 Ban Detected', '🚫 Ban Detected',
@@ -806,7 +806,7 @@ export class MicrosoftRewardsBot {
/** Send a strong alert to all channels and mention @everyone when entering global security standby. */ /** Send a strong alert to all channels and mention @everyone when entering global security standby. */
private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> { private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> {
try { try {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook') const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
await ConclusionWebhook( await ConclusionWebhook(
this.config, this.config,
'🚨 Critical Security Alert', '🚨 Critical Security Alert',

View File

@@ -6,8 +6,8 @@
*/ */
import type { BrowserContext } from 'rebrowser-playwright' import type { BrowserContext } from 'rebrowser-playwright'
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../../index'
import type { AccountProxy } from '../interface/Account' import type { AccountProxy } from '../../interface/Account'
/** /**
* Create a browser instance for the given account * Create a browser instance for the given account
@@ -26,7 +26,7 @@ export async function createBrowserInstance(
proxy: AccountProxy, proxy: AccountProxy,
email: string email: string
): Promise<BrowserContext> { ): Promise<BrowserContext> {
const browserModule = await import('../browser/Browser') const browserModule = await import('../../browser/Browser')
const Browser = browserModule.default const Browser = browserModule.default
const browserInstance = new Browser(bot) const browserInstance = new Browser(bot)
return await browserInstance.createBrowser(proxy, email) return await browserInstance.createBrowser(proxy, email)

View File

@@ -1,6 +1,6 @@
import { Page } from 'rebrowser-playwright' import { Page } from 'rebrowser-playwright'
import { Util } from './Utils' import type { ConfigHumanization } from '../../interface/Config'
import type { ConfigHumanization } from '../interface/Config' import { Util } from '../core/Utils'
export class Humanizer { export class Humanizer {
private util: Util private util: Util
@@ -46,9 +46,9 @@ export class Humanizer {
try { try {
const n = this.util.stringToMs(String(v)) const n = this.util.stringToMs(String(v))
return Math.max(0, Math.min(n, 10_000)) return Math.max(0, Math.min(n, 10_000))
} catch (e) { } catch (e) {
// Parse failed - use default minimum // Parse failed - use default minimum
return defMin return defMin
} }
} }
min = parse(this.cfg.actionDelay.min) min = parse(this.cfg.actionDelay.min)

View File

@@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator' import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../interface/UserAgentUtil' import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../../interface/UserAgentUtil'
import { log } from './Logger' import { Retry } from '../core/Retry'
import { Retry } from './Retry' import { log } from '../notifications/Logger'
interface UserAgentMetadata { interface UserAgentMetadata {
mobile: boolean mobile: boolean
@@ -95,7 +95,7 @@ export async function getChromeVersion(isMobile: boolean): Promise<string> {
export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionResult> { export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionResult> {
const now = Date.now() const now = Date.now()
// Return cached version if still valid // Return cached version if still valid
if (edgeVersionCache && edgeVersionCache.expiresAt > now) { if (edgeVersionCache && edgeVersionCache.expiresAt > now) {
return edgeVersionCache.data return edgeVersionCache.data
@@ -123,13 +123,13 @@ export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionRes
}) })
.catch(() => { .catch(() => {
edgeVersionInFlight = null edgeVersionInFlight = null
// Try stale cache first // Try stale cache first
if (edgeVersionCache) { if (edgeVersionCache) {
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using stale cached Edge versions due to fetch failure', 'warn') log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using stale cached Edge versions due to fetch failure', 'warn')
return edgeVersionCache.data return edgeVersionCache.data
} }
// Fall back to static versions // Fall back to static versions
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using static fallback Edge versions (API unavailable)', 'warn') log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using static fallback Edge versions (API unavailable)', 'warn')
edgeVersionCache = { data: FALLBACK_EDGE_VERSIONS, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS } edgeVersionCache = { data: FALLBACK_EDGE_VERSIONS, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS }
@@ -192,7 +192,7 @@ async function fetchEdgeVersionsWithRetry(isMobile: boolean): Promise<EdgeVersio
async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResult> { async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResult> {
let lastError: unknown = null let lastError: unknown = null
// Try axios first // Try axios first
try { try {
const response = await axios<EdgeVersion[]>({ const response = await axios<EdgeVersion[]>({
@@ -205,11 +205,11 @@ async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResu
timeout: 10000, timeout: 10000,
validateStatus: (status) => status === 200 validateStatus: (status) => status === 200
}) })
if (!response.data || !Array.isArray(response.data)) { if (!response.data || !Array.isArray(response.data)) {
throw new Error('Invalid response format from Edge API') throw new Error('Invalid response format from Edge API')
} }
return mapEdgeVersions(response.data) return mapEdgeVersions(response.data)
} catch (axiosError) { } catch (axiosError) {
lastError = axiosError lastError = axiosError
@@ -226,7 +226,7 @@ async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResu
} catch (fetchError) { } catch (fetchError) {
lastError = fetchError lastError = fetchError
} }
// Both methods failed // Both methods failed
const errorMsg = lastError instanceof Error ? lastError.message : String(lastError) const errorMsg = lastError instanceof Error ? lastError.message : String(lastError)
throw new Error(`Failed to fetch Edge versions: ${errorMsg}`) throw new Error(`Failed to fetch Edge versions: ${errorMsg}`)
@@ -237,7 +237,7 @@ async function tryNativeFetchFallback(): Promise<EdgeVersionResult | null> {
try { try {
const controller = new AbortController() const controller = new AbortController()
timeoutHandle = setTimeout(() => controller.abort(), 10000) timeoutHandle = setTimeout(() => controller.abort(), 10000)
const response = await fetch(EDGE_VERSION_URL, { const response = await fetch(EDGE_VERSION_URL, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -245,20 +245,20 @@ async function tryNativeFetchFallback(): Promise<EdgeVersionResult | null> {
}, },
signal: controller.signal signal: controller.signal
}) })
clearTimeout(timeoutHandle) clearTimeout(timeoutHandle)
timeoutHandle = undefined timeoutHandle = undefined
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`) throw new Error(`HTTP ${response.status}`)
} }
const data = await response.json() as EdgeVersion[] const data = await response.json() as EdgeVersion[]
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error('Invalid response format') throw new Error('Invalid response format')
} }
return mapEdgeVersions(data) return mapEdgeVersions(data)
} catch (error) { } catch (error) {
if (timeoutHandle) clearTimeout(timeoutHandle) if (timeoutHandle) clearTimeout(timeoutHandle)
@@ -270,24 +270,24 @@ function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
if (!Array.isArray(data) || data.length === 0) { if (!Array.isArray(data) || data.length === 0) {
throw new Error('Edge API returned empty or invalid data') throw new Error('Edge API returned empty or invalid data')
} }
const stable = data.find(entry => entry?.Product?.toLowerCase() === 'stable') const stable = data.find(entry => entry?.Product?.toLowerCase() === 'stable')
?? data.find(entry => entry?.Product && /stable/i.test(entry.Product)) ?? data.find(entry => entry?.Product && /stable/i.test(entry.Product))
if (!stable || !stable.Releases || !Array.isArray(stable.Releases)) { if (!stable || !stable.Releases || !Array.isArray(stable.Releases)) {
throw new Error('Stable Edge channel not found or invalid format') throw new Error('Stable Edge channel not found or invalid format')
} }
const androidRelease = stable.Releases.find(release => const androidRelease = stable.Releases.find(release =>
release?.Platform === Platform.Android && release?.ProductVersion release?.Platform === Platform.Android && release?.ProductVersion
) )
const windowsRelease = stable.Releases.find(release => const windowsRelease = stable.Releases.find(release =>
release?.Platform === Platform.Windows && release?.Platform === Platform.Windows &&
release?.Architecture === Architecture.X64 && release?.Architecture === Architecture.X64 &&
release?.ProductVersion release?.ProductVersion
) ?? stable.Releases.find(release => ) ?? stable.Releases.find(release =>
release?.Platform === Platform.Windows && release?.Platform === Platform.Windows &&
release?.ProductVersion release?.ProductVersion
) )
@@ -295,7 +295,7 @@ function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
android: androidRelease?.ProductVersion, android: androidRelease?.ProductVersion,
windows: windowsRelease?.ProductVersion windows: windowsRelease?.ProductVersion
} }
// Validate at least one version was found // Validate at least one version was found
if (!result.android && !result.windows) { if (!result.android && !result.windows) {
throw new Error('No valid Edge versions found in API response') throw new Error('No valid Edge versions found in API response')

View File

@@ -1,4 +1,4 @@
import type { ConfigRetryPolicy } from '../interface/Config' import type { ConfigRetryPolicy } from '../../interface/Config'
import { Util } from './Utils' import { Util } from './Utils'
type NumericPolicy = { type NumericPolicy = {
@@ -59,7 +59,7 @@ export class Retry {
let attempt = 0 let attempt = 0
let delay = this.policy.baseDelay let delay = this.policy.baseDelay
let lastErr: unknown let lastErr: unknown
while (attempt < this.policy.maxAttempts) { while (attempt < this.policy.maxAttempts) {
try { try {
return await fn() return await fn()

View File

@@ -2,7 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } f
import { HttpProxyAgent } from 'http-proxy-agent' import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent' import { SocksProxyAgent } from 'socks-proxy-agent'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../../interface/Account'
class AxiosClient { class AxiosClient {
private instance: AxiosInstance private instance: AxiosInstance
@@ -90,13 +90,13 @@ class AxiosClient {
// FIXED: Initialize lastError to prevent throwing undefined // FIXED: Initialize lastError to prevent throwing undefined
let lastError: unknown = new Error('Request failed with unknown error') let lastError: unknown = new Error('Request failed with unknown error')
const maxAttempts = 2 const maxAttempts = 2
for (let attempt = 1; attempt <= maxAttempts; attempt++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try { try {
return await this.instance.request(config) return await this.instance.request(config)
} catch (err: unknown) { } catch (err: unknown) {
lastError = err lastError = err
// Handle HTTP 407 Proxy Authentication Required // Handle HTTP 407 Proxy Authentication Required
if (this.isProxyAuthError(err)) { if (this.isProxyAuthError(err)) {
// Retry without proxy on auth failure // Retry without proxy on auth failure
@@ -116,15 +116,15 @@ class AxiosClient {
const bypassInstance = axios.create() const bypassInstance = axios.create()
return bypassInstance.request(config) return bypassInstance.request(config)
} }
// Non-retryable error // Non-retryable error
throw err throw err
} }
} }
throw lastError throw lastError
} }
/** /**
* Check if error is HTTP 407 Proxy Authentication Required * Check if error is HTTP 407 Proxy Authentication Required
*/ */
@@ -132,27 +132,27 @@ class AxiosClient {
const axiosErr = err as AxiosError | undefined const axiosErr = err as AxiosError | undefined
return axiosErr?.response?.status === 407 return axiosErr?.response?.status === 407
} }
/** /**
* Check if error is retryable (network/proxy issues) * Check if error is retryable (network/proxy issues)
*/ */
private isRetryableError(err: unknown): boolean { private isRetryableError(err: unknown): boolean {
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
if (!e) return false if (!e) return false
const code = e.code || e.cause?.code const code = e.code || e.cause?.code
const isNetworkError = code === 'ECONNREFUSED' || const isNetworkError = code === 'ECONNREFUSED' ||
code === 'ETIMEDOUT' || code === 'ETIMEDOUT' ||
code === 'ECONNRESET' || code === 'ECONNRESET' ||
code === 'ENOTFOUND' || code === 'ENOTFOUND' ||
code === 'EPIPE' code === 'EPIPE'
const msg = String(e.message || '') const msg = String(e.message || '')
const isProxyIssue = /proxy|tunnel|socks|agent/i.test(msg) const isProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
return isNetworkError || isProxyIssue return isNetworkError || isProxyIssue
} }
private sleep(ms: number): Promise<void> { private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
} }

View File

@@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { Util } from './Utils' import { Util } from '../core/Utils'
export interface QueryDiversityConfig { export interface QueryDiversityConfig {
sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'> sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>
@@ -22,10 +22,10 @@ export class QueryDiversityEngine {
constructor(config?: Partial<QueryDiversityConfig>, logger?: (source: string, message: string, level?: 'info' | 'warn' | 'error') => void) { constructor(config?: Partial<QueryDiversityConfig>, logger?: (source: string, message: string, level?: 'info' | 'warn' | 'error') => void) {
const maxQueriesPerSource = Math.max(1, Math.min(config?.maxQueriesPerSource || 10, 50)) const maxQueriesPerSource = Math.max(1, Math.min(config?.maxQueriesPerSource || 10, 50))
const cacheMinutes = Math.max(1, Math.min(config?.cacheMinutes || 30, 1440)) const cacheMinutes = Math.max(1, Math.min(config?.cacheMinutes || 30, 1440))
this.config = { this.config = {
sources: config?.sources && config.sources.length > 0 sources: config?.sources && config.sources.length > 0
? config.sources ? config.sources
: ['google-trends', 'reddit', 'local-fallback'], : ['google-trends', 'reddit', 'local-fallback'],
deduplicate: config?.deduplicate !== false, deduplicate: config?.deduplicate !== false,
mixStrategies: config?.mixStrategies !== false, mixStrategies: config?.mixStrategies !== false,
@@ -44,7 +44,7 @@ export class QueryDiversityEngine {
/** /**
* Generic HTTP fetch with error handling and timeout * Generic HTTP fetch with error handling and timeout
*/ */
private async fetchHttp(url: string, config?: { private async fetchHttp(url: string, config?: {
method?: 'GET' | 'POST' method?: 'GET' | 'POST'
headers?: Record<string, string> headers?: Record<string, string>
data?: string data?: string
@@ -104,7 +104,7 @@ export class QueryDiversityEngine {
*/ */
private async getFromSource(source: string): Promise<string[]> { private async getFromSource(source: string): Promise<string[]> {
this.cleanExpiredCache() this.cleanExpiredCache()
const cached = this.cache.get(source) const cached = this.cache.get(source)
if (cached && Date.now() < cached.expires) { if (cached && Date.now() < cached.expires) {
return cached.queries return cached.queries
@@ -174,7 +174,7 @@ export class QueryDiversityEngine {
try { try {
const subreddits = ['news', 'worldnews', 'todayilearned', 'askreddit', 'technology'] const subreddits = ['news', 'worldnews', 'todayilearned', 'askreddit', 'technology']
const randomSub = subreddits[Math.floor(Math.random() * subreddits.length)] const randomSub = subreddits[Math.floor(Math.random() * subreddits.length)]
const data = await this.fetchHttp(`https://www.reddit.com/r/${randomSub}/hot.json?limit=15`) const data = await this.fetchHttp(`https://www.reddit.com/r/${randomSub}/hot.json?limit=15`)
const parsed = JSON.parse(data) const parsed = JSON.parse(data)
const posts = parsed.data?.children || [] const posts = parsed.data?.children || []
@@ -296,28 +296,28 @@ export class QueryDiversityEngine {
const result: string[] = [] const result: string[] = []
const queriesPerSource = Math.ceil(this.config.maxQueriesPerSource) const queriesPerSource = Math.ceil(this.config.maxQueriesPerSource)
const sourceCount = this.config.sources.length const sourceCount = this.config.sources.length
if (sourceCount === 0 || queries.length === 0) { if (sourceCount === 0 || queries.length === 0) {
return queries.slice(0, targetCount) return queries.slice(0, targetCount)
} }
const chunkSize = queriesPerSource const chunkSize = queriesPerSource
let sourceIndex = 0 let sourceIndex = 0
for (let i = 0; i < queries.length && result.length < targetCount; i++) { for (let i = 0; i < queries.length && result.length < targetCount; i++) {
const currentChunkStart = sourceIndex * chunkSize const currentChunkStart = sourceIndex * chunkSize
const currentChunkEnd = currentChunkStart + chunkSize const currentChunkEnd = currentChunkStart + chunkSize
const query = queries[i] const query = queries[i]
if (query && i >= currentChunkStart && i < currentChunkEnd) { if (query && i >= currentChunkStart && i < currentChunkEnd) {
result.push(query) result.push(query)
} }
if (i === currentChunkEnd - 1) { if (i === currentChunkEnd - 1) {
sourceIndex = (sourceIndex + 1) % sourceCount sourceIndex = (sourceIndex + 1) % sourceCount
} }
} }
return result.slice(0, targetCount) return result.slice(0, targetCount)
} }

View File

@@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import { Config } from '../interface/Config' import { DISCORD } from '../../constants'
import { Ntfy } from './Ntfy' import { Config } from '../../interface/Config'
import { log } from './Logger' import { log } from './Logger'
import { DISCORD } from '../constants' import { Ntfy } from './Ntfy'
interface DiscordField { interface DiscordField {
name: string name: string

View File

@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import { DISCORD } from '../constants' import { DISCORD } from '../../constants'
import { Config } from '../interface/Config' import { Config } from '../../interface/Config'
interface ErrorReportPayload { interface ErrorReportPayload {
error: string error: string
@@ -35,7 +35,7 @@ export function deobfuscateWebhookUrl(encoded: string): string {
*/ */
function shouldReportError(errorMessage: string): boolean { function shouldReportError(errorMessage: string): boolean {
const lowerMessage = errorMessage.toLowerCase() const lowerMessage = errorMessage.toLowerCase()
// List of patterns that indicate user configuration errors (not reportable bugs) // List of patterns that indicate user configuration errors (not reportable bugs)
const userConfigPatterns = [ const userConfigPatterns = [
/accounts\.jsonc.*not found/i, /accounts\.jsonc.*not found/i,
@@ -59,14 +59,14 @@ function shouldReportError(errorMessage: string): boolean {
/session closed.*rebrowser/i, /session closed.*rebrowser/i,
/addScriptToEvaluateOnNewDocument.*session closed/i /addScriptToEvaluateOnNewDocument.*session closed/i
] ]
// Don't report user configuration errors // Don't report user configuration errors
for (const pattern of userConfigPatterns) { for (const pattern of userConfigPatterns) {
if (pattern.test(lowerMessage)) { if (pattern.test(lowerMessage)) {
return false return false
} }
} }
// List of patterns that indicate expected/handled errors (not bugs) // List of patterns that indicate expected/handled errors (not bugs)
const expectedErrorPatterns = [ const expectedErrorPatterns = [
/no.*points.*to.*earn/i, /no.*points.*to.*earn/i,
@@ -76,14 +76,14 @@ function shouldReportError(errorMessage: string): boolean {
/quest.*not.*found/i, /quest.*not.*found/i,
/promotion.*expired/i /promotion.*expired/i
] ]
// Don't report expected/handled errors // Don't report expected/handled errors
for (const pattern of expectedErrorPatterns) { for (const pattern of expectedErrorPatterns) {
if (pattern.test(lowerMessage)) { if (pattern.test(lowerMessage)) {
return false return false
} }
} }
// Report everything else (genuine bugs) // Report everything else (genuine bugs)
return true return true
} }
@@ -111,7 +111,7 @@ export async function sendErrorReport(
} }
const errorMessage = error instanceof Error ? error.message : String(error) const errorMessage = error instanceof Error ? error.message : String(error)
// Filter out false positives and user configuration errors // Filter out false positives and user configuration errors
if (!shouldReportError(errorMessage)) { if (!shouldReportError(errorMessage)) {
return return

View File

@@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import chalk from 'chalk' import chalk from 'chalk'
import { DISCORD, LOGGER_CLEANUP } from '../constants' import { DISCORD, LOGGER_CLEANUP } from '../../constants'
import { loadConfig } from '../state/Load'
import { sendErrorReport } from './ErrorReportingWebhook' import { sendErrorReport } from './ErrorReportingWebhook'
import { loadConfig } from './Load'
import { Ntfy } from './Ntfy' import { Ntfy } from './Ntfy'
/** /**

View File

@@ -1,5 +1,5 @@
import { loadConfig } from './Load'
import axios from 'axios' import axios from 'axios'
import { loadConfig } from '../state/Load'
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
error: { priority: 'max', tags: 'rotating_light' }, // Customize the ERROR icon here, see: https://docs.ntfy.sh/emojis/ error: { priority: 'max', tags: 'rotating_light' }, // Customize the ERROR icon here, see: https://docs.ntfy.sh/emojis/

View File

@@ -1,6 +1,6 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import type { Config } from '../interface/Config' import type { Config } from '../../interface/Config'
type AccountCompletionMeta = { type AccountCompletionMeta = {
runId?: string runId?: string

View File

@@ -2,9 +2,9 @@ import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { BrowserContext, Cookie } from 'rebrowser-playwright' import { BrowserContext, Cookie } from 'rebrowser-playwright'
import { Account } from '../interface/Account' import { Account } from '../../interface/Account'
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../interface/Config' import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../../interface/Config'
import { Util } from './Utils' import { Util } from '../core/Utils'
const utils = new Util() const utils = new Util()
@@ -76,16 +76,16 @@ function normalizeConfig(raw: unknown): Config {
if (!raw || typeof raw !== 'object') { if (!raw || typeof raw !== 'object') {
throw new Error('Config must be a valid object') throw new Error('Config must be a valid object')
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const n = raw as Record<string, any> const n = raw as Record<string, any>
// Browser settings // Browser settings
const browserConfig = n.browser ?? {} const browserConfig = n.browser ?? {}
const headless = process.env.FORCE_HEADLESS === '1' const headless = process.env.FORCE_HEADLESS === '1'
? true ? true
: (typeof browserConfig.headless === 'boolean' : (typeof browserConfig.headless === 'boolean'
? browserConfig.headless ? browserConfig.headless
: (typeof n.headless === 'boolean' ? n.headless : false)) // Legacy fallback : (typeof n.headless === 'boolean' ? n.headless : false)) // Legacy fallback
const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s' const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s'
@@ -339,12 +339,12 @@ export function loadAccounts(): Account[] {
] ]
let chosen: string | null = null let chosen: string | null = null
for (const p of candidates) { for (const p of candidates) {
try { try {
if (fs.existsSync(p)) { if (fs.existsSync(p)) {
chosen = p chosen = p
break break
} }
} catch (e) { } catch (e) {
// Filesystem check failed for this path, try next // Filesystem check failed for this path, try next
continue continue
} }
@@ -365,12 +365,12 @@ export function loadAccounts(): Account[] {
if (!entry || typeof entry !== 'object') { if (!entry || typeof entry !== 'object') {
throw new Error('each account entry must be an object') throw new Error('each account entry must be an object')
} }
// Use Record<string, any> to access dynamic properties from untrusted JSON // Use Record<string, any> to access dynamic properties from untrusted JSON
// Runtime validation below ensures type safety // Runtime validation below ensures type safety
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const a = entry as Record<string, any> const a = entry as Record<string, any>
// Validate required fields with proper type checking // Validate required fields with proper type checking
if (typeof a.email !== 'string' || typeof a.password !== 'string') { if (typeof a.email !== 'string' || typeof a.password !== 'string') {
throw new Error('each account must have email and password strings') throw new Error('each account must have email and password strings')
@@ -439,15 +439,15 @@ export function loadConfig(): Config {
candidates.push(path.join(base, name)) candidates.push(path.join(base, name))
} }
} }
let cfgPath: string | null = null let cfgPath: string | null = null
for (const p of candidates) { for (const p of candidates) {
try { try {
if (fs.existsSync(p)) { if (fs.existsSync(p)) {
cfgPath = p cfgPath = p
break break
} }
} catch (e) { } catch (e) {
// Filesystem check failed for this path, try next // Filesystem check failed for this path, try next
continue continue
} }
@@ -517,7 +517,7 @@ export async function saveSessionData(sessionPath: string, browser: BrowserConte
// Save cookies to a file // Save cookies to a file
await fs.promises.writeFile( await fs.promises.writeFile(
path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`), path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`),
JSON.stringify(cookies, null, 2) JSON.stringify(cookies, null, 2)
) )

View File

@@ -1,9 +1,9 @@
import chalk from 'chalk' import chalk from 'chalk'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { Account } from '../interface/Account' import { Account } from '../../interface/Account'
import { Config } from '../interface/Config' import { Config } from '../../interface/Config'
import { log } from './Logger' import { log } from '../notifications/Logger'
interface ValidationError { interface ValidationError {
severity: 'error' | 'warning' severity: 'error' | 'warning'
@@ -181,12 +181,12 @@ export class StartupValidator {
private validateConfig(config: Config): void { private validateConfig(config: Config): void {
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
if (maybeSchedule !== undefined) { if (maybeSchedule !== undefined) {
this.addWarning( this.addWarning(
'config', 'config',
'Legacy schedule settings detected in config.jsonc.', 'Legacy schedule settings detected in config.jsonc.',
'Remove schedule.* entries and use your operating system scheduler.', 'Remove schedule.* entries and use your operating system scheduler.',
'docs/schedule.md' 'docs/schedule.md'
) )
} }
// Headless mode in Docker // Headless mode in Docker
@@ -218,10 +218,10 @@ export class StartupValidator {
} }
// Global timeout validation // Global timeout validation
const timeout = typeof config.globalTimeout === 'string' const timeout = typeof config.globalTimeout === 'string'
? config.globalTimeout ? config.globalTimeout
: `${config.globalTimeout}ms` : `${config.globalTimeout}ms`
if (timeout === '0' || timeout === '0ms' || timeout === '0s') { if (timeout === '0' || timeout === '0ms' || timeout === '0s') {
this.addError( this.addError(
'config', 'config',
@@ -271,7 +271,7 @@ export class StartupValidator {
// Node.js version check // Node.js version check
const nodeVersion = process.version const nodeVersion = process.version
const major = parseInt(nodeVersion.split('.')[0]?.replace('v', '') || '0', 10) const major = parseInt(nodeVersion.split('.')[0]?.replace('v', '') || '0', 10)
if (major < 18) { if (major < 18) {
this.addError( this.addError(
'environment', 'environment',
@@ -329,10 +329,10 @@ export class StartupValidator {
// Check job-state directory if enabled // Check job-state directory if enabled
if (config.jobState?.enabled !== false) { if (config.jobState?.enabled !== false) {
const jobStateDir = config.jobState?.dir const jobStateDir = config.jobState?.dir
? config.jobState.dir ? config.jobState.dir
: path.join(sessionPath, 'job-state') : path.join(sessionPath, 'job-state')
if (!fs.existsSync(jobStateDir)) { if (!fs.existsSync(jobStateDir)) {
try { try {
fs.mkdirSync(jobStateDir, { recursive: true }) fs.mkdirSync(jobStateDir, { recursive: true })
@@ -428,12 +428,12 @@ export class StartupValidator {
private validateWorkerSettings(config: Config): void { private validateWorkerSettings(config: Config): void {
const workers = config.workers const workers = config.workers
// Check if at least one worker is enabled // Check if at least one worker is enabled
const anyEnabled = workers.doDailySet || workers.doMorePromotions || workers.doPunchCards || const anyEnabled = workers.doDailySet || workers.doMorePromotions || workers.doPunchCards ||
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn || workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
workers.doReadToEarn workers.doReadToEarn
if (!anyEnabled) { if (!anyEnabled) {
this.addWarning( this.addWarning(
'workers', 'workers',
@@ -465,7 +465,7 @@ export class StartupValidator {
private validateExecutionSettings(config: Config): void { private validateExecutionSettings(config: Config): void {
// Validate passesPerRun // Validate passesPerRun
const passes = config.passesPerRun ?? 1 const passes = config.passesPerRun ?? 1
if (passes < 1) { if (passes < 1) {
this.addError( this.addError(
'execution', 'execution',
@@ -595,8 +595,8 @@ export class StartupValidator {
// Action delays // Action delays
if (human.actionDelay) { if (human.actionDelay) {
const minMs = typeof human.actionDelay.min === 'string' const minMs = typeof human.actionDelay.min === 'string'
? parseInt(human.actionDelay.min, 10) ? parseInt(human.actionDelay.min, 10)
: human.actionDelay.min : human.actionDelay.min
const maxMs = typeof human.actionDelay.max === 'string' const maxMs = typeof human.actionDelay.max === 'string'
? parseInt(human.actionDelay.max, 10) ? parseInt(human.actionDelay.max, 10)
@@ -717,7 +717,7 @@ export class StartupValidator {
const errorLabel = this.errors.length === 1 ? 'error' : 'errors' const errorLabel = this.errors.length === 1 ? 'error' : 'errors'
const warningLabel = this.warnings.length === 1 ? 'warning' : 'warnings' const warningLabel = this.warnings.length === 1 ? 'warning' : 'warnings'
log('main', 'VALIDATION', `[${this.errors.length > 0 ? 'ERROR' : 'OK'}] Found: ${this.errors.length} ${errorLabel} | ${this.warnings.length} ${warningLabel}`) log('main', 'VALIDATION', `[${this.errors.length > 0 ? 'ERROR' : 'OK'}] Found: ${this.errors.length} ${errorLabel} | ${this.warnings.length} ${warningLabel}`)
if (this.errors.length > 0) { if (this.errors.length > 0) {
log('main', 'VALIDATION', 'Bot will continue, but issues may cause failures', 'warn') log('main', 'VALIDATION', 'Bot will continue, but issues may cause failures', 'warn')
log('main', 'VALIDATION', 'Full documentation: docs/index.md') log('main', 'VALIDATION', 'Full documentation: docs/index.md')