New structure

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
scripts/README.md Normal file
View File

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

View File

View File

@@ -2,7 +2,7 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import * as readline from 'readline' import * as readline from 'readline'
import type { BrowserContext, Page } from 'rebrowser-playwright' import type { BrowserContext, Page } from 'rebrowser-playwright'
import { log } from '../util/Logger' import { log } from '../util/notifications/Logger'
import { DataGenerator } from './DataGenerator' import { DataGenerator } from './DataGenerator'
import { CreatedAccount } from './types' import { CreatedAccount } from './types'
@@ -363,7 +363,7 @@ export class AccountCreator {
// STEP 2: Wait for DOM to be fully loaded // STEP 2: Wait for DOM to be fully loaded
// Silent catch justified: DOMContentLoaded may already be complete // Silent catch justified: DOMContentLoaded may already be complete
await this.page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => {}) await this.page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => { })
// STEP 3: REDUCED delay - pages load fast // STEP 3: REDUCED delay - pages load fast
await this.humanDelay(1500, 2500) await this.humanDelay(1500, 2500)
@@ -383,7 +383,7 @@ export class AccountCreator {
if (visible) { if (visible) {
// Silent catch justified: Loading indicators may disappear before timeout, which is fine // Silent catch justified: Loading indicators may disappear before timeout, which is fine
await element.waitFor({ state: 'hidden', timeout: Math.min(5000, maxWaitMs - (Date.now() - startTime)) }).catch(() => {}) await element.waitFor({ state: 'hidden', timeout: Math.min(5000, maxWaitMs - (Date.now() - startTime)) }).catch(() => { })
} }
} }
@@ -669,7 +669,7 @@ export class AccountCreator {
this.rl.close() this.rl.close()
this.rlClosed = true this.rlClosed = true
} }
} catch {/* ignore */} } catch {/* ignore */ }
} }
} }
@@ -905,7 +905,7 @@ export class AccountCreator {
if (retryCount >= MAX_EMAIL_RETRIES) { if (retryCount >= MAX_EMAIL_RETRIES) {
log(false, 'CREATOR', `❌ Max email retries (${MAX_EMAIL_RETRIES}) reached - giving up`, 'error') log(false, 'CREATOR', `❌ Max email retries (${MAX_EMAIL_RETRIES}) reached - giving up`, 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
@@ -940,7 +940,7 @@ export class AccountCreator {
log(false, 'CREATOR', 'Unknown error type, pausing for inspection', 'error') log(false, 'CREATOR', 'Unknown error type, pausing for inspection', 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
@@ -1082,7 +1082,7 @@ export class AccountCreator {
if (suggestionButtons.length === 0) { if (suggestionButtons.length === 0) {
log(false, 'CREATOR', 'Suggestions container found but no buttons inside', 'error') log(false, 'CREATOR', 'Suggestions container found but no buttons inside', 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
@@ -1091,7 +1091,7 @@ export class AccountCreator {
if (!firstButton) { if (!firstButton) {
log(false, 'CREATOR', 'First button is undefined', 'error') log(false, 'CREATOR', 'First button is undefined', 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
@@ -1107,7 +1107,7 @@ export class AccountCreator {
if (!cleanEmail) { if (!cleanEmail) {
log(false, 'CREATOR', 'Could not extract email from suggestion button', 'error') log(false, 'CREATOR', 'Could not extract email from suggestion button', 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
@@ -1146,7 +1146,7 @@ export class AccountCreator {
if (finalError) { if (finalError) {
log(false, 'CREATOR', 'Failed to resolve error', 'error') log(false, 'CREATOR', 'Failed to resolve error', 'error')
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
await new Promise(() => {}) await new Promise(() => { })
return { success: false, email: null } return { success: false, email: null }
} }
} }

View File

@@ -1,6 +1,6 @@
import Browser from '../browser/Browser' import Browser from '../browser/Browser'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { log } from '../util/Logger' import { log } from '../util/notifications/Logger'
import { AccountCreator } from './AccountCreator' import { AccountCreator } from './AccountCreator'
async function main(): Promise<void> { async function main(): Promise<void> {
@@ -109,7 +109,7 @@ async function main(): Promise<void> {
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

View File

@@ -4,8 +4,8 @@ import playwright, { BrowserContext } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../interface/Account'
import { loadSessionData, saveFingerprintData } from '../util/Load' import { updateFingerprintUserAgent } from '../util/browser/UserAgent'
import { updateFingerprintUserAgent } from '../util/UserAgent' import { loadSessionData, saveFingerprintData } from '../util/state/Load'
class Browser { class Browser {
private bot: MicrosoftRewardsBot private bot: MicrosoftRewardsBot
@@ -106,7 +106,7 @@ 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')

View File

@@ -8,7 +8,7 @@ import { AppUserData } from '../interface/AppUserData'
import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData' import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
import { EarnablePoints } from '../interface/Points' import { EarnablePoints } from '../interface/Points'
import { QuizData } from '../interface/QuizData' import { QuizData } from '../interface/QuizData'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/state/Load'
export default class BrowserFunc { export default class BrowserFunc {
@@ -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
} }

View File

@@ -1,7 +1,7 @@
import { load } from 'cheerio' import { load } from 'cheerio'
import { Page } from 'rebrowser-playwright' import { Page } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { logError } from '../util/Logger' import { logError } from '../util/notifications/Logger'
type DismissButton = { selector: string; label: string; isXPath?: boolean } type DismissButton = { selector: string; label: string; isXPath?: boolean }

View File

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

View File

@@ -1,6 +1,6 @@
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import { log as botLog } from '../util/Logger' import { getErrorMessage } from '../util/core/Utils'
import { getErrorMessage } from '../util/Utils' import { log as botLog } from '../util/notifications/Logger'
import { dashboardState } from './state' import { dashboardState } from './state'
export class BotController { export class BotController {

View File

@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express' import { Request, Response, Router } from 'express'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { getConfigPath, loadAccounts, loadConfig } from '../util/Load' import { getConfigPath, loadAccounts, loadConfig } from '../util/state/Load'
import { botController } from './BotController' import { botController } from './BotController'
import { dashboardState } from './state' import { dashboardState } from './state'

View File

@@ -3,7 +3,7 @@ import fs from 'fs'
import { createServer } from 'http' import { createServer } from 'http'
import path from 'path' import path from 'path'
import { WebSocket, WebSocketServer } from 'ws' import { WebSocket, WebSocketServer } from 'ws'
import { log as botLog } from '../util/Logger' import { log as botLog } from '../util/notifications/Logger'
import { apiRouter } from './routes' import { apiRouter } from './routes'
import { DashboardLog, dashboardState } from './state' import { DashboardLog, dashboardState } from './state'

View File

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

View File

@@ -4,7 +4,7 @@
*/ */
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/state/Load'
/** /**
* Handle compromised/security check mode for an account * Handle compromised/security check mode for an account
@@ -38,7 +38,7 @@ export async function handleCompromisedMode(
// 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',

View File

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

View File

@@ -10,10 +10,10 @@
*/ */
import type { Config } from '../interface/Config' import type { Config } from '../interface/Config'
import { ConclusionWebhook } from '../util/ConclusionWebhook' import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
import { JobState } from '../util/JobState' import { log } from '../util/notifications/Logger'
import { log } from '../util/Logger' import { Ntfy } from '../util/notifications/Ntfy'
import { Ntfy } from '../util/Ntfy' import { JobState } from '../util/state/JobState'
export interface AccountResult { export interface AccountResult {
email: string email: string

View File

@@ -5,11 +5,11 @@ import readline from 'readline'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { OAuth } from '../interface/OAuth' import { OAuth } from '../interface/OAuth'
import { saveSessionData } from '../util/Load' import { Retry } from '../util/core/Retry'
import { logError } from '../util/Logger' import { logError } from '../util/notifications/Logger'
import { LoginState, LoginStateDetector } from '../util/LoginStateDetector' import { generateTOTP } from '../util/security/Totp'
import { Retry } from '../util/Retry' import { saveSessionData } from '../util/state/Load'
import { generateTOTP } from '../util/Totp' import { LoginState, LoginStateDetector } from '../util/validation/LoginStateDetector'
// ------------------------------- // -------------------------------
// REFACTORING NOTE (1700+ lines) // REFACTORING NOTE (1700+ lines)
@@ -380,7 +380,7 @@ export class Login {
isRetryable isRetryable
) )
const data: OAuth = resp.data const data: OAuth = resp.data
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now()-start)/1000)}s`) this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now() - start) / 1000)}s`)
this.currentTotpSecret = undefined this.currentTotpSecret = undefined
return data.access_token return data.access_token
} catch (error) { } catch (error) {
@@ -473,7 +473,7 @@ export class Login {
if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) { if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) {
await this.handlePasskeyPrompts(page, 'main') await this.handlePasskeyPrompts(page, 'main')
} }
} catch {/* ignore reuse errors and continue with full login */} } catch {/* ignore reuse errors and continue with full login */ }
return false return false
} }
@@ -517,7 +517,7 @@ export class Login {
// Step 3: Recovery mismatch check // Step 3: Recovery mismatch check
await this.tryRecoveryMismatchCheck(page, email) await this.tryRecoveryMismatchCheck(page, email)
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'recovery-mismatch') { if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'recovery-mismatch') {
this.bot.log(this.bot.isMobile,'LOGIN','Recovery mismatch detected stopping before password entry','warn') this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery mismatch detected stopping before password entry', 'warn')
return return
} }
@@ -544,7 +544,7 @@ export class Login {
// IMPROVEMENT: Wait for page to be fully ready before looking for email field // IMPROVEMENT: Wait for page to be fully ready before looking for email field
// Silent catch justified: DOMContentLoaded may already be complete, which is fine // Silent catch justified: DOMContentLoaded may already be complete, which is fine
await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {}) await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(300) // Extra settling time await this.bot.utils.wait(300) // Extra settling time
if (await this.tryAutoTotp(page, 'pre-email check')) { if (await this.tryAutoTotp(page, 'pre-email check')) {
@@ -552,14 +552,14 @@ export class Login {
} }
// IMPROVEMENT: More retries with better timing // IMPROVEMENT: More retries with better timing
let field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(()=>null) // Increased from 5000ms let field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(() => null) // Increased from 5000ms
if (!field) { if (!field) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found (first attempt), retrying...', 'warn') this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found (first attempt), retrying...', 'warn')
const totpHandled = await this.tryAutoTotp(page, 'pre-email challenge') const totpHandled = await this.tryAutoTotp(page, 'pre-email challenge')
if (totpHandled) { if (totpHandled) {
await this.bot.utils.wait(1200) // Increased from 800ms await this.bot.utils.wait(1200) // Increased from 800ms
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(()=>null) field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 8000 }).catch(() => null)
} }
} }
@@ -574,7 +574,7 @@ export class Login {
if (content.length < 1000) { if (content.length < 1000) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page content too small, reloading...', 'warn') this.bot.log(this.bot.isMobile, 'LOGIN', 'Page content too small, reloading...', 'warn')
// Silent catch justified: Reload may timeout if page is slow, but we continue anyway // Silent catch justified: Reload may timeout if page is slow, but we continue anyway
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => {}) await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => { })
await this.bot.utils.wait(1500) await this.bot.utils.wait(1500)
} }
@@ -583,10 +583,10 @@ export class Login {
await this.bot.utils.wait(1200) // Increased from 800ms await this.bot.utils.wait(1200) // Increased from 800ms
} }
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null) field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(() => null)
if (!field && this.totpAttempts > 0) { if (!field && this.totpAttempts > 0) {
await this.bot.utils.wait(2500) // Increased from 2000ms await this.bot.utils.wait(2500) // Increased from 2000ms
field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null) // Increased from 3000ms field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(() => null) // Increased from 3000ms
} }
if (!field) { if (!field) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present after all retries', 'error') this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present after all retries', 'error')
@@ -594,14 +594,14 @@ export class Login {
} }
} }
const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(()=>null) const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(() => null)
if (!prefilled) { if (!prefilled) {
await page.fill(SELECTORS.emailInput, '') await page.fill(SELECTORS.emailInput, '')
await page.fill(SELECTORS.emailInput, email) await page.fill(SELECTORS.emailInput, email)
} else { } else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled') this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled')
} }
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null) const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
if (next) { if (next) {
await next.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Email submit click failed: ${e}`, 'warn')) await next.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Email submit click failed: ${e}`, 'warn'))
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email')
@@ -614,7 +614,7 @@ export class Login {
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
// Some flows require switching to password first // Some flows require switching to password first
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null) const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(() => null)
if (switchBtn) { if (switchBtn) {
await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn')) await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn'))
await this.bot.utils.wait(1000) await this.bot.utils.wait(1000)
@@ -630,12 +630,12 @@ export class Login {
} }
// Rare flow: list of methods -> choose password // Rare flow: list of methods -> choose password
let passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(()=>null) let passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(() => null)
if (!passwordField) { if (!passwordField) {
// Maybe passkey prompt appeared - try handling it again // Maybe passkey prompt appeared - try handling it again
await this.handlePasskeyPrompts(page, 'main') await this.handlePasskeyPrompts(page, 'main')
await this.bot.utils.wait(800) await this.bot.utils.wait(800)
passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(()=>null) passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(() => null)
} }
if (!passwordField) { if (!passwordField) {
@@ -652,7 +652,7 @@ export class Login {
await page.fill(SELECTORS.passwordInput, '') await page.fill(SELECTORS.passwordInput, '')
await page.fill(SELECTORS.passwordInput, password) await page.fill(SELECTORS.passwordInput, password)
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null) const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
if (submit) { if (submit) {
await submit.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Password submit failed: ${e}`, 'warn')) await submit.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Password submit failed: ${e}`, 'warn'))
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted')
@@ -686,7 +686,7 @@ export class Login {
if (this.bot.config.parallel) { if (this.bot.config.parallel) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Parallel mode: throttling authenticator push requests', 'log', 'yellow') this.bot.log(this.bot.isMobile, 'LOGIN', 'Parallel mode: throttling authenticator push requests', 'log', 'yellow')
for (let attempts = 0; attempts < 6; attempts++) { // max 6 minutes retry window for (let attempts = 0; attempts < 6; attempts++) { // max 6 minutes retry window
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null) const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(() => null)
if (!resend) break if (!resend) break
await this.bot.utils.wait(60000) await this.bot.utils.wait(60000)
await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile)) await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile))
@@ -710,14 +710,14 @@ export class Login {
return return
} catch { } catch {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired refreshing') this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired refreshing')
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null) const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(() => null)
if (retryBtn) await retryBtn.click().catch(logError('LOGIN-AUTH', 'Refresh button click failed', this.bot.isMobile)) if (retryBtn) await retryBtn.click().catch(logError('LOGIN-AUTH', 'Refresh button click failed', this.bot.isMobile))
const refreshed = await this.fetchAuthenticatorNumber(page) const refreshed = await this.fetchAuthenticatorNumber(page)
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return } if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
numberToPress = refreshed numberToPress = refreshed
} }
} }
this.bot.log(this.bot.isMobile,'LOGIN','Authenticator approval loop exited (max cycles reached)','warn') this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator approval loop exited (max cycles reached)', 'warn')
} }
private async handleSMSOrTotp(page: Page) { private async handleSMSOrTotp(page: Page) {
@@ -809,7 +809,7 @@ export class Login {
try { try {
const code = generateTOTP(this.currentTotpSecret!.trim()) const code = generateTOTP(this.currentTotpSecret!.trim())
const input = page.locator(selector).first() const input = page.locator(selector).first()
if (!await input.isVisible().catch(()=>false)) { if (!await input.isVisible().catch(() => false)) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn') this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn')
return return
} }
@@ -953,21 +953,21 @@ export class Login {
if (el && el.textContent) texts.push(el.textContent) if (el && el.textContent) texts.push(el.textContent)
}) })
return texts.join(' ') return texts.join(' ')
}).catch(()=>'') }).catch(() => '')
if (labelText && /code|otp|authenticator|sécurité|securité|security/i.test(labelText)) return true if (labelText && /code|otp|authenticator|sécurité|securité|security/i.test(labelText)) return true
if (headingHint && /code|otp|authenticator/i.test(headingHint.toLowerCase())) return true if (headingHint && /code|otp|authenticator/i.test(headingHint.toLowerCase())) return true
} catch {/* fall through to false */} } catch {/* fall through to false */ }
return false return false
} }
private async detectTotpHeading(page: Page): Promise<string | null> { private async detectTotpHeading(page: Page): Promise<string | null> {
const headings = page.locator('[data-testid="title"], h1, h2, div[role="heading"]') const headings = page.locator('[data-testid="title"], h1, h2, div[role="heading"]')
const count = await headings.count().catch(()=>0) const count = await headings.count().catch(() => 0)
const max = Math.min(count, 6) const max = Math.min(count, 6)
for (let i = 0; i < max; i++) { for (let i = 0; i < max; i++) {
const text = (await headings.nth(i).textContent().catch(()=>null))?.trim() const text = (await headings.nth(i).textContent().catch(() => null))?.trim()
if (!text) continue if (!text) continue
const lowered = text.toLowerCase() const lowered = text.toLowerCase()
if (/authenticator/.test(lowered) && /code/.test(lowered)) return text if (/authenticator/.test(lowered) && /code/.test(lowered)) return text
@@ -1042,7 +1042,7 @@ export class Login {
return true return true
} }
} }
} catch {/* ignore */} } catch {/* ignore */ }
} }
return false return false
}).catch(() => false), }).catch(() => false),
@@ -1052,7 +1052,7 @@ export class Login {
try { try {
const el = document.querySelector(sel) const el = document.querySelector(sel)
if (el && (el as HTMLElement).offsetParent !== null) return true if (el && (el as HTMLElement).offsetParent !== null) return true
} catch {/* ignore */} } catch {/* ignore */ }
} }
return false return false
}).catch(() => false) }).catch(() => false)
@@ -1072,7 +1072,7 @@ export class Login {
if (checkCount % 2 === 0) { // Every other iteration if (checkCount % 2 === 0) { // Every other iteration
for (const sel of selectors) { for (const sel of selectors) {
const loc = page.locator(sel).first() const loc = page.locator(sel).first()
if (await loc.isVisible().catch(()=>false)) { if (await loc.isVisible().catch(() => false)) {
return sel return sel
} }
} }
@@ -1295,9 +1295,9 @@ export class Login {
} }
private async checkAccountLocked(page: Page) { private async checkAccountLocked(page: Page) {
const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(()=>true).catch(()=>false) const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(() => true).catch(() => false)
if (locked) { if (locked) {
this.bot.log(this.bot.isMobile,'CHECK-LOCKED','Account locked by Microsoft (serviceAbuseLandingTitle)','error') this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'Account locked by Microsoft (serviceAbuseLandingTitle)', 'error')
throw new Error('Account locked by Microsoft - please review account status') throw new Error('Account locked by Microsoft - please review account status')
} }
} }
@@ -1307,7 +1307,7 @@ export class Login {
let did = false let did = false
// Priority 1: Direct detection of "Skip for now" button by data-testid // Priority 1: Direct detection of "Skip for now" button by data-testid
const skipBtn = await page.waitForSelector('button[data-testid="secondaryButton"]', { timeout: 500 }).catch(()=>null) const skipBtn = await page.waitForSelector('button[data-testid="secondaryButton"]', { timeout: 500 }).catch(() => null)
if (skipBtn) { if (skipBtn) {
const text = (await skipBtn.textContent() || '').trim() const text = (await skipBtn.textContent() || '').trim()
// Check if it's actually a skip button (could be other secondary buttons) // Check if it's actually a skip button (could be other secondary buttons)
@@ -1320,7 +1320,7 @@ export class Login {
// Priority 2: Video heuristic (biometric prompt) // Priority 2: Video heuristic (biometric prompt)
if (!did) { if (!did) {
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null) const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(() => null)
if (biometric) { if (biometric) {
const btn = await page.$(SELECTORS.passkeySecondary) const btn = await page.$(SELECTORS.passkeySecondary)
if (btn) { if (btn) {
@@ -1333,18 +1333,18 @@ export class Login {
// Priority 3: Title + secondary button detection // Priority 3: Title + secondary button detection
if (!did) { if (!did) {
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(()=>null) const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(() => null)
const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(()=>null) const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(() => null)
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null) const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(() => null)
const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || '' const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title) const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title)
if (looksLike && secBtn) { if (looksLike && secBtn) {
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile)) await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile))
did = true did = true
this.logPasskeyOnce('title heuristic '+title) this.logPasskeyOnce('title heuristic ' + title)
} }
else if (!did && secBtn && primBtn) { else if (!did && secBtn && primBtn) {
const text = (await secBtn.textContent()||'').trim() const text = (await secBtn.textContent() || '').trim()
if (/skip for now|not now|later|passer|plus tard/i.test(text)) { if (/skip for now|not now|later|passer|plus tard/i.test(text)) {
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile)) await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile))
did = true did = true
@@ -1356,7 +1356,7 @@ export class Login {
// Priority 4: XPath fallback (includes Windows Hello specific patterns) // Priority 4: XPath fallback (includes Windows Hello specific patterns)
if (!did) { if (!did) {
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first() const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first()
if (await textBtn.isVisible().catch(()=>false)) { if (await textBtn.isVisible().catch(() => false)) {
await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile)) await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile))
did = true did = true
this.logPasskeyOnce('xpath fallback') this.logPasskeyOnce('xpath fallback')
@@ -1399,13 +1399,13 @@ export class Login {
} }
// KMSI prompt // KMSI prompt
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null) const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(() => null)
if (kmsi) { if (kmsi) {
const yes = await page.$(SELECTORS.passkeyPrimary) const yes = await page.$(SELECTORS.passkeyPrimary)
if (yes) { if (yes) {
await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile)) await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile))
did = true did = true
this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') this.bot.log(this.bot.isMobile, 'LOGIN-KMSI', 'Accepted KMSI prompt')
} }
} }
@@ -1414,7 +1414,7 @@ export class Login {
const now = Date.now() const now = Date.now()
if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) { if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) {
this.lastNoPromptLog = now this.lastNoPromptLog = now
this.bot.log(this.bot.isMobile,'LOGIN-NO-PROMPT',`No dialogs (x${this.noPromptIterations})`) this.bot.log(this.bot.isMobile, 'LOGIN-NO-PROMPT', `No dialogs (x${this.noPromptIterations})`)
if (this.noPromptIterations > 50) this.noPromptIterations = 0 if (this.noPromptIterations > 50) this.noPromptIterations = 0
} }
} else if (did) { } else if (did) {
@@ -1425,7 +1425,7 @@ export class Login {
private logPasskeyOnce(reason: string) { private logPasskeyOnce(reason: string) {
if (this.passkeyHandled) return if (this.passkeyHandled) return
this.passkeyHandled = true this.passkeyHandled = true
this.bot.log(this.bot.isMobile,'LOGIN-PASSKEY',`Dismissed passkey prompt (${reason})`) this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
} }
// --------------- Security Detection --------------- // --------------- Security Detection ---------------
@@ -1433,11 +1433,11 @@ export class Login {
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'sign-in-blocked') return true if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'sign-in-blocked') return true
try { try {
let text = '' let text = ''
for (const sel of ['[data-testid="title"]','h1','div[role="heading"]','div.text-title']) { for (const sel of ['[data-testid="title"]', 'h1', 'div[role="heading"]', 'div.text-title']) {
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(()=>null) const el = await page.waitForSelector(sel, { timeout: 600 }).catch(() => null)
if (el) { if (el) {
const t = (await el.textContent()||'').trim() const t = (await el.textContent() || '').trim()
if (t && t.length < 300) text += ' '+t if (t && t.length < 300) text += ' ' + t
} }
} }
const lower = text.toLowerCase() const lower = text.toLowerCase()
@@ -1453,7 +1453,7 @@ export class Login {
next: ['Manual recovery required before continuing'], next: ['Manual recovery required before continuing'],
docsUrl docsUrl
} }
await this.sendIncidentAlert(incident,'warn') await this.sendIncidentAlert(incident, 'warn')
this.bot.compromisedModeActive = true this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'sign-in-blocked' this.bot.compromisedReason = 'sign-in-blocked'
this.startCompromisedInterval() this.startCompromisedInterval()
@@ -1477,32 +1477,32 @@ export class Login {
const recoveryEmail: string | undefined = this.bot.currentAccountRecoveryEmail const recoveryEmail: string | undefined = this.bot.currentAccountRecoveryEmail
if (!recoveryEmail || !/@/.test(recoveryEmail)) return if (!recoveryEmail || !/@/.test(recoveryEmail)) return
const accountEmail = email const accountEmail = email
const parseRef = (val: string) => { const [l,d] = val.split('@'); return { local: l||'', domain:(d||'').toLowerCase(), prefix2:(l||'').slice(0,2).toLowerCase() } } const parseRef = (val: string) => { const [l, d] = val.split('@'); return { local: l || '', domain: (d || '').toLowerCase(), prefix2: (l || '').slice(0, 2).toLowerCase() } }
const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r=>r.domain && r.prefix2) const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r => r.domain && r.prefix2)
if (refs.length === 0) return if (refs.length === 0) return
const candidates: string[] = [] const candidates: string[] = []
// Direct selectors (Microsoft variants + French spans) // Direct selectors (Microsoft variants + French spans)
const sel = '[data-testid="recoveryEmailHint"], #recoveryEmail, [id*="ProofEmail"], [id*="EmailProof"], [data-testid*="Email"], span:has(span.fui-Text)' const sel = '[data-testid="recoveryEmailHint"], #recoveryEmail, [id*="ProofEmail"], [id*="EmailProof"], [data-testid*="Email"], span:has(span.fui-Text)'
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(()=>null) const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(() => null)
if (el) { const t = (await el.textContent()||'').trim(); if (t) candidates.push(t) } if (el) { const t = (await el.textContent() || '').trim(); if (t) candidates.push(t) }
// List items // List items
const li = page.locator('[role="listitem"], li') const li = page.locator('[role="listitem"], li')
const liCount = await li.count().catch(()=>0) const liCount = await li.count().catch(() => 0)
for (let i=0;i<liCount && i<12;i++) { const t = (await li.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && /@/.test(t)) candidates.push(t) } for (let i = 0; i < liCount && i < 12; i++) { const t = (await li.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && /@/.test(t)) candidates.push(t) }
// XPath generic masked patterns // XPath generic masked patterns
const xp = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]') const xp = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]')
const xpCount = await xp.count().catch(()=>0) const xpCount = await xp.count().catch(() => 0)
for (let i=0;i<xpCount && i<12;i++) { const t = (await xp.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && t.length<300) candidates.push(t) } for (let i = 0; i < xpCount && i < 12; i++) { const t = (await xp.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && t.length < 300) candidates.push(t) }
// Normalize // Normalize
const seen = new Set<string>() const seen = new Set<string>()
const norm = (s:string)=>s.replace(/\s+/g,' ').trim() const norm = (s: string) => s.replace(/\s+/g, ' ').trim()
const uniq = candidates.map(norm).filter(t=>t && !seen.has(t) && seen.add(t)) const uniq = candidates.map(norm).filter(t => t && !seen.has(t) && seen.add(t))
// Masked filter // Masked filter
let masked = uniq.filter(t=>/@/.test(t) && /[*•]/.test(t)) let masked = uniq.filter(t => /@/.test(t) && /[*•]/.test(t))
if (masked.length === 0) { if (masked.length === 0) {
// Fallback full HTML scan // Fallback full HTML scan
@@ -1513,14 +1513,14 @@ export class Login {
const found = new Set<string>() const found = new Set<string>()
let m: RegExpExecArray | null let m: RegExpExecArray | null
while ((m = generic.exec(html)) !== null) found.add(m[0]) while ((m = generic.exec(html)) !== null) found.add(m[0])
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g,'').trim(); if (raw) found.add(raw) } while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) }
if (found.size > 0) masked = Array.from(found) if (found.size > 0) masked = Array.from(found)
} catch {/* ignore */} } catch {/* ignore */ }
} }
if (masked.length === 0) return if (masked.length === 0) return
// Prefer one mentioning email/adresse // Prefer one mentioning email/adresse
const preferred = masked.find(t=>/email|courriel|adresse|mail/i.test(t)) || masked[0]! const preferred = masked.find(t => /email|courriel|adresse|mail/i.test(t)) || masked[0]!
// Extract the masked email: Microsoft sometimes shows only first 1 char (k*****@domain) or 2 chars (ko*****@domain). // Extract the masked email: Microsoft sometimes shows only first 1 char (k*****@domain) or 2 chars (ko*****@domain).
// We ONLY compare (1 or 2) leading visible alphanumeric chars + full domain (case-insensitive). // We ONLY compare (1 or 2) leading visible alphanumeric chars + full domain (case-insensitive).
// This avoids false positives when the displayed mask hides the 2nd char. // This avoids false positives when the displayed mask hides the 2nd char.
@@ -1539,7 +1539,7 @@ export class Login {
} }
if (!observedPrefix && extractedLower.includes('@')) { if (!observedPrefix && extractedLower.includes('@')) {
const parts = extractedLower.split('@') const parts = extractedLower.split('@')
observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi,'').slice(0,2) observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi, '').slice(0, 2)
} }
// Determine if any reference (recoveryEmail or accountEmail) matches observed mask logic // Determine if any reference (recoveryEmail or accountEmail) matches observed mask logic
@@ -1555,22 +1555,22 @@ export class Login {
if (!matchRef) { if (!matchRef) {
const docsUrl = this.getDocsUrl('recovery-email-mismatch') const docsUrl = this.getDocsUrl('recovery-email-mismatch')
const incident: SecurityIncident = { const incident: SecurityIncident = {
kind:'Recovery email mismatch', kind: 'Recovery email mismatch',
account: email, account: email,
details:[ details: [
`MaskedShown: ${preferred}`, `MaskedShown: ${preferred}`,
`Extracted: ${extracted}`, `Extracted: ${extracted}`,
`Observed => ${observedPrefix || '??'}**@${observedDomain || '??'}`, `Observed => ${observedPrefix || '??'}**@${observedDomain || '??'}`,
`Expected => ${refs.map(r=>`${r.prefix2}**@${r.domain}`).join(' OR ')}` `Expected => ${refs.map(r => `${r.prefix2}**@${r.domain}`).join(' OR ')}`
], ],
next:[ next: [
'Automation halted globally (standby engaged).', 'Automation halted globally (standby engaged).',
'Verify account security & recovery email in Microsoft settings.', 'Verify account security & recovery email in Microsoft settings.',
'Update accounts.json if the change was legitimate before restart.' 'Update accounts.json if the change was legitimate before restart.'
], ],
docsUrl docsUrl
} }
await this.sendIncidentAlert(incident,'critical') await this.sendIncidentAlert(incident, 'critical')
this.bot.compromisedModeActive = true this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'recovery-mismatch' this.bot.compromisedReason = 'recovery-mismatch'
this.startCompromisedInterval() this.startCompromisedInterval()
@@ -1578,32 +1578,32 @@ export class Login {
await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-RECOVERY', 'Failed to open docs tab', this.bot.isMobile)) await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-RECOVERY', 'Failed to open docs tab', this.bot.isMobile))
} else { } else {
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict' const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`) this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
} }
} catch {/* non-fatal */} } catch {/* non-fatal */ }
} }
private async switchToPasswordLink(page: Page) { private async switchToPasswordLink(page: Page) {
try { try {
const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first() const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first()
if (await link.isVisible().catch(()=>false)) { if (await link.isVisible().catch(() => false)) {
await link.click().catch(logError('LOGIN', 'Use password link click failed', this.bot.isMobile)) await link.click().catch(logError('LOGIN', 'Use password link click failed', this.bot.isMobile))
await this.bot.utils.wait(800) await this.bot.utils.wait(800)
this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link') this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link')
} }
} catch {/* ignore */} } catch {/* ignore */ }
} }
// --------------- Incident Helpers --------------- // --------------- Incident Helpers ---------------
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn'|'critical'='warn') { private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn' | 'critical' = 'warn') {
const lines = [ `[Incident] ${incident.kind}`, `Account: ${incident.account}` ] const lines = [`[Incident] ${incident.kind}`, `Account: ${incident.account}`]
if (incident.details?.length) lines.push(`Details: ${incident.details.join(' | ')}`) if (incident.details?.length) lines.push(`Details: ${incident.details.join(' | ')}`)
if (incident.next?.length) lines.push(`Next: ${incident.next.join(' -> ')}`) if (incident.next?.length) lines.push(`Next: ${incident.next.join(' -> ')}`)
if (incident.docsUrl) lines.push(`Docs: ${incident.docsUrl}`) if (incident.docsUrl) lines.push(`Docs: ${incident.docsUrl}`)
const level: 'warn'|'error' = severity === 'critical' ? 'error' : 'warn' const level: 'warn' | 'error' = severity === 'critical' ? 'error' : 'warn'
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level) this.bot.log(this.bot.isMobile, 'SECURITY', lines.join(' | '), level)
try { try {
const { ConclusionWebhook } = await import('../util/ConclusionWebhook') const { ConclusionWebhook } = await import('../util/notifications/ConclusionWebhook')
const fields = [ const fields = [
{ name: 'Account', value: incident.account }, { name: 'Account', value: incident.account },
...(incident.details?.length ? [{ name: 'Details', value: incident.details.join('\n') }] : []), ...(incident.details?.length ? [{ name: 'Details', value: incident.details.join('\n') }] : []),
@@ -1617,14 +1617,14 @@ export class Login {
fields, fields,
severity === 'critical' ? 0xFF0000 : 0xFFAA00 severity === 'critical' ? 0xFF0000 : 0xFFAA00
) )
} catch {/* ignore */} } catch {/* ignore */ }
} }
private getDocsUrl(anchor?: string) { private getDocsUrl(anchor?: string) {
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/Obsidian-wtf/Microsoft-Rewards-Bot/blob/main/docs/security.md' const base = process.env.DOCS_BASE?.trim() || 'https://github.com/Obsidian-wtf/Microsoft-Rewards-Bot/blob/main/docs/security.md'
const map: Record<string,string> = { const map: Record<string, string> = {
'recovery-email-mismatch':'#recovery-email-mismatch', 'recovery-email-mismatch': '#recovery-email-mismatch',
'we-cant-sign-you-in':'#we-cant-sign-you-in-blocked' 'we-cant-sign-you-in': '#we-cant-sign-you-in-blocked'
} }
return anchor && map[anchor] ? `${base}${map[anchor]}` : base return anchor && map[anchor] ? `${base}${map[anchor]}` : base
} }
@@ -1635,9 +1635,9 @@ export class Login {
clearInterval(this.compromisedInterval) clearInterval(this.compromisedInterval)
this.compromisedInterval = undefined this.compromisedInterval = undefined
} }
this.compromisedInterval = setInterval(()=>{ this.compromisedInterval = setInterval(() => {
try { try {
this.bot.log(this.bot.isMobile,'SECURITY','Security standby active. Manual review required before proceeding.','warn') this.bot.log(this.bot.isMobile, 'SECURITY', 'Security standby active. Manual review required before proceeding.', 'warn')
} catch { } catch {
// Intentionally silent: If logging fails in interval, don't crash the timer // Intentionally silent: If logging fails in interval, don't crash the timer
// The interval will try again in 5 minutes // The interval will try again in 5 minutes
@@ -1657,7 +1657,7 @@ export class Login {
const ctx = page.context() const ctx = page.context()
const tab = await ctx.newPage() const tab = await ctx.newPage()
await tab.goto(url, { waitUntil: 'domcontentloaded' }) await tab.goto(url, { waitUntil: 'domcontentloaded' })
} catch {/* ignore */} } catch {/* ignore */ }
} }
// --------------- Infrastructure --------------- // --------------- Infrastructure ---------------

View File

@@ -4,10 +4,10 @@ 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 = {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Page } from 'rebrowser-playwright' import { Page } from 'rebrowser-playwright'
import { Util } from './Utils' import type { ConfigHumanization } from '../../interface/Config'
import type { ConfigHumanization } from '../interface/Config' import { Util } from '../core/Utils'
export class Humanizer { export class Humanizer {
private util: Util private util: Util

View File

@@ -1,8 +1,8 @@
import axios from 'axios' import axios from 'axios'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator' import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../interface/UserAgentUtil' import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../../interface/UserAgentUtil'
import { log } from './Logger' import { Retry } from '../core/Retry'
import { Retry } from './Retry' import { log } from '../notifications/Logger'
interface UserAgentMetadata { interface UserAgentMetadata {
mobile: boolean mobile: boolean

View File

@@ -1,4 +1,4 @@
import type { ConfigRetryPolicy } from '../interface/Config' import type { ConfigRetryPolicy } from '../../interface/Config'
import { Util } from './Utils' import { Util } from './Utils'
type NumericPolicy = { type NumericPolicy = {

View File

@@ -2,7 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } f
import { HttpProxyAgent } from 'http-proxy-agent' import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent' import { SocksProxyAgent } from 'socks-proxy-agent'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../../interface/Account'
class AxiosClient { class AxiosClient {
private instance: AxiosInstance private instance: AxiosInstance

View File

@@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { Util } from './Utils' import { Util } from '../core/Utils'
export interface QueryDiversityConfig { export interface QueryDiversityConfig {
sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'> sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>

View File

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

View File

@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import { DISCORD } from '../constants' import { DISCORD } from '../../constants'
import { Config } from '../interface/Config' import { Config } from '../../interface/Config'
interface ErrorReportPayload { interface ErrorReportPayload {
error: string error: string

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { BrowserContext, Cookie } from 'rebrowser-playwright' import { BrowserContext, Cookie } from 'rebrowser-playwright'
import { Account } from '../interface/Account' import { Account } from '../../interface/Account'
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../interface/Config' import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../../interface/Config'
import { Util } from './Utils' import { Util } from '../core/Utils'
const utils = new Util() const utils = new Util()

View File

@@ -1,9 +1,9 @@
import chalk from 'chalk' import chalk from 'chalk'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { Account } from '../interface/Account' import { Account } from '../../interface/Account'
import { Config } from '../interface/Config' import { Config } from '../../interface/Config'
import { log } from './Logger' import { log } from '../notifications/Logger'
interface ValidationError { interface ValidationError {
severity: 'error' | 'warning' severity: 'error' | 'warning'