mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
New structure
This commit is contained in:
@@ -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/
|
||||||
|
|||||||
57
.github/copilot-instructions.md
vendored
57
.github/copilot-instructions.md
vendored
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
22
scripts/README.md
Normal 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.
|
||||||
0
flake.lock → setup/nix/flake.lock
generated
0
flake.lock → setup/nix/flake.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}"]`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
24
src/index.ts
24
src/index.ts
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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')
|
||||||
@@ -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()
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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/
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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')
|
||||||
Reference in New Issue
Block a user