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
|
||||
setup/
|
||||
|
||||
# Docker files (no recursion)
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
compose.yaml
|
||||
# Docker files (organized in docker/ folder - no recursion needed)
|
||||
docker/
|
||||
.dockerignore
|
||||
|
||||
# NixOS specific files (not needed in Docker)
|
||||
flake.nix
|
||||
flake.lock
|
||||
run.sh
|
||||
# Scripts (organized in scripts/ folder - not needed in Docker)
|
||||
scripts/
|
||||
|
||||
# Asset files (not needed for runtime)
|
||||
assets/
|
||||
|
||||
57
.github/copilot-instructions.md
vendored
57
.github/copilot-instructions.md
vendored
@@ -44,17 +44,33 @@ src/
|
||||
│ ├── Poll.ts # Poll completion
|
||||
│ ├── ThisOrThat.ts # This or That game
|
||||
│ └── ...
|
||||
├── util/ # Shared utilities + infrastructure
|
||||
│ ├── Axios.ts # HTTP client with proxy support
|
||||
│ ├── BrowserFactory.ts # Centralized browser creation
|
||||
│ ├── Humanizer.ts # Random delays, mouse gestures
|
||||
│ ├── BanDetector.ts # Heuristic ban detection
|
||||
│ ├── QueryDiversityEngine.ts # Multi-source search query generation
|
||||
│ ├── JobState.ts # Persistent job state tracking
|
||||
│ ├── Logger.ts # Centralized logging with redaction
|
||||
│ ├── Retry.ts # Exponential backoff retry logic
|
||||
│ ├── Utils.ts # General-purpose helpers
|
||||
│ └── ...
|
||||
├── util/ # Shared utilities (ORGANIZED BY CATEGORY)
|
||||
│ ├── core/ # Core utilities
|
||||
│ │ ├── Utils.ts # General-purpose helpers
|
||||
│ │ └── Retry.ts # Exponential backoff retry logic
|
||||
│ ├── network/ # HTTP & API utilities
|
||||
│ │ ├── Axios.ts # HTTP client with proxy support
|
||||
│ │ └── 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
|
||||
│ ├── 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)
|
||||
│ ├── server.ts # Express server + routes
|
||||
│ ├── routes.ts # API endpoints
|
||||
@@ -73,9 +89,20 @@ src/
|
||||
├── nameDatabase.ts # First/last name pool
|
||||
├── types.ts # Account creation interfaces
|
||||
└── 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.bat # Windows setup script
|
||||
├── setup.sh # Linux/Mac setup script
|
||||
├── nix/ # NixOS configuration
|
||||
│ ├── flake.nix # Nix flake definition
|
||||
│ └── flake.lock # Nix flake lock file
|
||||
└── update/
|
||||
├── setup.mjs # Initial setup automation
|
||||
└── 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
|
||||
|
||||
**entrypoint.sh:**
|
||||
- **Purpose:** Docker container initialization script
|
||||
**docker/entrypoint.sh:**
|
||||
- **Purpose:** Docker container initialization script (located in `docker/` directory)
|
||||
- **Key Features:**
|
||||
- Timezone configuration (env: `TZ`, default UTC)
|
||||
- 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`)
|
||||
- **Usage:** Docker Compose sets `CRON_SCHEDULE`, container runs cron in foreground
|
||||
|
||||
**run_daily.sh:**
|
||||
- **Purpose:** Daily execution wrapper for cron jobs
|
||||
**docker/run_daily.sh:**
|
||||
- **Purpose:** Daily execution wrapper for cron jobs (located in `docker/` directory)
|
||||
- **Key Features:**
|
||||
- Random sleep delay (0-30min) to avoid simultaneous runs across containers
|
||||
- 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 runtime scripts with proper permissions and normalize line endings for non-Unix users
|
||||
COPY --chmod=755 src/run_daily.sh ./src/run_daily.sh
|
||||
COPY --chmod=644 src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
|
||||
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
# IMPROVED: Scripts now organized in docker/ folder
|
||||
COPY --chmod=755 docker/run_daily.sh ./docker/run_daily.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 \
|
||||
&& 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 ["/usr/local/bin/entrypoint.sh"]
|
||||
@@ -1,3 +1,3 @@
|
||||
# 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
|
||||
}
|
||||
# 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] 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
@@ -2,7 +2,7 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as readline from 'readline'
|
||||
import type { BrowserContext, Page } from 'rebrowser-playwright'
|
||||
import { log } from '../util/Logger'
|
||||
import { log } from '../util/notifications/Logger'
|
||||
import { DataGenerator } from './DataGenerator'
|
||||
import { CreatedAccount } from './types'
|
||||
|
||||
@@ -41,8 +41,8 @@ export class AccountCreator {
|
||||
if (!domain) return false
|
||||
const lowerDomain = domain.toLowerCase()
|
||||
return lowerDomain === 'outlook.com' ||
|
||||
lowerDomain === 'hotmail.com' ||
|
||||
lowerDomain === 'outlook.fr'
|
||||
lowerDomain === 'hotmail.com' ||
|
||||
lowerDomain === 'outlook.fr'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,7 +363,7 @@ export class AccountCreator {
|
||||
|
||||
// STEP 2: Wait for DOM to be fully loaded
|
||||
// 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
|
||||
await this.humanDelay(1500, 2500)
|
||||
@@ -383,7 +383,7 @@ export class AccountCreator {
|
||||
|
||||
if (visible) {
|
||||
// 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.rlClosed = true
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,7 +905,7 @@ export class AccountCreator {
|
||||
if (retryCount >= MAX_EMAIL_RETRIES) {
|
||||
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')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
|
||||
@@ -934,13 +934,13 @@ export class AccountCreator {
|
||||
|
||||
// Check for email taken error
|
||||
if (errorText && (errorText.toLowerCase().includes('taken') || errorText.toLowerCase().includes('pris') ||
|
||||
errorText.toLowerCase().includes('already') || errorText.toLowerCase().includes('déjà'))) {
|
||||
errorText.toLowerCase().includes('already') || errorText.toLowerCase().includes('déjà'))) {
|
||||
return await this.handleEmailTaken(retryCount)
|
||||
}
|
||||
|
||||
log(false, 'CREATOR', 'Unknown error type, pausing for inspection', 'error')
|
||||
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
|
||||
@@ -1082,7 +1082,7 @@ export class AccountCreator {
|
||||
if (suggestionButtons.length === 0) {
|
||||
log(false, 'CREATOR', 'Suggestions container found but no buttons inside', 'error')
|
||||
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,7 @@ export class AccountCreator {
|
||||
if (!firstButton) {
|
||||
log(false, 'CREATOR', 'First button is undefined', 'error')
|
||||
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
|
||||
@@ -1107,7 +1107,7 @@ export class AccountCreator {
|
||||
if (!cleanEmail) {
|
||||
log(false, 'CREATOR', 'Could not extract email from suggestion button', 'error')
|
||||
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
|
||||
@@ -1146,7 +1146,7 @@ export class AccountCreator {
|
||||
if (finalError) {
|
||||
log(false, 'CREATOR', 'Failed to resolve error', 'error')
|
||||
log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow')
|
||||
await new Promise(() => {})
|
||||
await new Promise(() => { })
|
||||
return { success: false, email: null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Browser from '../browser/Browser'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { log } from '../util/Logger'
|
||||
import { log } from '../util/notifications/Logger'
|
||||
import { AccountCreator } from './AccountCreator'
|
||||
|
||||
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')
|
||||
|
||||
// Keep process alive indefinitely
|
||||
await new Promise(() => {}) // Never resolves
|
||||
await new Promise(() => { }) // Never resolves
|
||||
} else {
|
||||
// Failure
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
@@ -4,8 +4,8 @@ import playwright, { BrowserContext } from 'rebrowser-playwright'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||
import { updateFingerprintUserAgent } from '../util/UserAgent'
|
||||
import { updateFingerprintUserAgent } from '../util/browser/UserAgent'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/state/Load'
|
||||
|
||||
class Browser {
|
||||
private bot: MicrosoftRewardsBot
|
||||
@@ -106,7 +106,7 @@ class Browser {
|
||||
}
|
||||
`
|
||||
document.documentElement.appendChild(style)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
})
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup 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 { EarnablePoints } from '../interface/Points'
|
||||
import { QuizData } from '../interface/QuizData'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { saveSessionData } from '../util/state/Load'
|
||||
|
||||
|
||||
export default class BrowserFunc {
|
||||
@@ -212,7 +212,7 @@ export default class BrowserFunc {
|
||||
if (msg.includes('has been closed')) {
|
||||
if (attempt === 1) {
|
||||
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 {
|
||||
break
|
||||
}
|
||||
@@ -545,10 +545,10 @@ export default class BrowserFunc {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
if (element) {
|
||||
selector = `a[href*="${element.attribs.href}"]`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { load } from 'cheerio'
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { logError } from '../util/Logger'
|
||||
import { logError } from '../util/notifications/Logger'
|
||||
|
||||
type DismissButton = { selector: string; label: string; isXPath?: boolean }
|
||||
|
||||
@@ -151,7 +151,7 @@ export default class BrowserUtil {
|
||||
const titleByText = page.locator('h1').filter({ hasText: titleText })
|
||||
|
||||
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
|
||||
|
||||
@@ -200,8 +200,8 @@ export default class BrowserUtil {
|
||||
|
||||
const isNetworkError = $('body.neterror').length
|
||||
const hasHttp400Error = html.includes('HTTP ERROR 400') ||
|
||||
html.includes('This page isn\'t working') ||
|
||||
html.includes('This page is not working')
|
||||
html.includes('This page isn\'t working') ||
|
||||
html.includes('This page is not working')
|
||||
|
||||
if (isNetworkError || hasHttp400Error) {
|
||||
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)
|
||||
if (!Number.isFinite(parsed)) {
|
||||
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')
|
||||
}).catch(() => {
|
||||
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) {
|
||||
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')
|
||||
}).catch(() => {
|
||||
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 { log as botLog } from '../util/Logger'
|
||||
import { getErrorMessage } from '../util/Utils'
|
||||
import { getErrorMessage } from '../util/core/Utils'
|
||||
import { log as botLog } from '../util/notifications/Logger'
|
||||
import { dashboardState } from './state'
|
||||
|
||||
export class BotController {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getConfigPath, loadAccounts, loadConfig } from '../util/Load'
|
||||
import { getConfigPath, loadAccounts, loadConfig } from '../util/state/Load'
|
||||
import { botController } from './BotController'
|
||||
import { dashboardState } from './state'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs'
|
||||
import { createServer } from 'http'
|
||||
import path from 'path'
|
||||
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 { DashboardLog, dashboardState } from './state'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { Account } from '../interface/Account'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface DesktopFlowResult {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { saveSessionData } from '../util/state/Load'
|
||||
|
||||
/**
|
||||
* Handle compromised/security check mode for an account
|
||||
@@ -38,7 +38,7 @@ export async function handleCompromisedMode(
|
||||
|
||||
// Send security alert webhook
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('../util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
bot.config,
|
||||
isMobile ? '🔐 Security Check (Mobile)' : '🔐 Security Check',
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { Account } from '../interface/Account'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory'
|
||||
import { MobileRetryTracker } from '../util/MobileRetryTracker'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
|
||||
import { MobileRetryTracker } from '../util/state/MobileRetryTracker'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface MobileFlowResult {
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
*/
|
||||
|
||||
import type { Config } from '../interface/Config'
|
||||
import { ConclusionWebhook } from '../util/ConclusionWebhook'
|
||||
import { JobState } from '../util/JobState'
|
||||
import { log } from '../util/Logger'
|
||||
import { Ntfy } from '../util/Ntfy'
|
||||
import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
|
||||
import { log } from '../util/notifications/Logger'
|
||||
import { Ntfy } from '../util/notifications/Ntfy'
|
||||
import { JobState } from '../util/state/JobState'
|
||||
|
||||
export interface AccountResult {
|
||||
email: string
|
||||
|
||||
@@ -5,11 +5,11 @@ import readline from 'readline'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { OAuth } from '../interface/OAuth'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { logError } from '../util/Logger'
|
||||
import { LoginState, LoginStateDetector } from '../util/LoginStateDetector'
|
||||
import { Retry } from '../util/Retry'
|
||||
import { generateTOTP } from '../util/Totp'
|
||||
import { Retry } from '../util/core/Retry'
|
||||
import { logError } from '../util/notifications/Logger'
|
||||
import { generateTOTP } from '../util/security/Totp'
|
||||
import { saveSessionData } from '../util/state/Load'
|
||||
import { LoginState, LoginStateDetector } from '../util/validation/LoginStateDetector'
|
||||
|
||||
// -------------------------------
|
||||
// REFACTORING NOTE (1700+ lines)
|
||||
@@ -209,8 +209,8 @@ export class Login {
|
||||
await this.bot.utils.wait(DEFAULT_TIMEOUTS.fastPoll)
|
||||
const content = await page.content().catch(() => '')
|
||||
const hasHttp400 = content.includes('HTTP ERROR 400') ||
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
|
||||
if (hasHttp400) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 detected in content, reloading...', 'warn')
|
||||
@@ -299,8 +299,8 @@ export class Login {
|
||||
return ''
|
||||
})
|
||||
const hasHttp400 = content.includes('HTTP ERROR 400') ||
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
|
||||
if (hasHttp400) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'HTTP 400 detected, reloading...', 'warn')
|
||||
@@ -362,8 +362,8 @@ export class Login {
|
||||
const err = e as { response?: { status?: number }; code?: string }
|
||||
const status = err.response?.status
|
||||
return status === 502 || status === 503 || status === 504 ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'ETIMEDOUT'
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'ETIMEDOUT'
|
||||
}
|
||||
|
||||
const req: AxiosRequestConfig = {
|
||||
@@ -380,7 +380,7 @@ export class Login {
|
||||
isRetryable
|
||||
)
|
||||
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
|
||||
return data.access_token
|
||||
} catch (error) {
|
||||
@@ -420,8 +420,8 @@ export class Login {
|
||||
await this.bot.utils.wait(DEFAULT_TIMEOUTS.fastPoll)
|
||||
const content = await page.content().catch(() => '')
|
||||
const hasHttp400 = content.includes('HTTP ERROR 400') ||
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
|
||||
if (hasHttp400) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'HTTP 400 on session check, reloading...', 'warn')
|
||||
@@ -473,7 +473,7 @@ export class Login {
|
||||
if (currentUrl.includes('login.live.com') || currentUrl.includes('login.microsoftonline.com')) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ export class Login {
|
||||
// Step 3: Recovery mismatch check
|
||||
await this.tryRecoveryMismatchCheck(page, email)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ export class Login {
|
||||
|
||||
// IMPROVEMENT: Wait for page to be fully ready before looking for email field
|
||||
// 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
|
||||
|
||||
if (await this.tryAutoTotp(page, 'pre-email check')) {
|
||||
@@ -552,14 +552,14 @@ export class Login {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found (first attempt), retrying...', 'warn')
|
||||
|
||||
const totpHandled = await this.tryAutoTotp(page, 'pre-email challenge')
|
||||
if (totpHandled) {
|
||||
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) {
|
||||
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
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => {})
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => { })
|
||||
await this.bot.utils.wait(1500)
|
||||
}
|
||||
|
||||
@@ -583,10 +583,10 @@ export class Login {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
await page.fill(SELECTORS.emailInput, '')
|
||||
await page.fill(SELECTORS.emailInput, email)
|
||||
} else {
|
||||
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) {
|
||||
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')
|
||||
@@ -614,7 +614,7 @@ export class Login {
|
||||
await this.bot.utils.wait(500)
|
||||
|
||||
// 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) {
|
||||
await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn'))
|
||||
await this.bot.utils.wait(1000)
|
||||
@@ -630,12 +630,12 @@ export class Login {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Maybe passkey prompt appeared - try handling it again
|
||||
await this.handlePasskeyPrompts(page, 'main')
|
||||
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) {
|
||||
@@ -652,7 +652,7 @@ export class Login {
|
||||
|
||||
await page.fill(SELECTORS.passwordInput, '')
|
||||
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) {
|
||||
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')
|
||||
@@ -666,10 +666,10 @@ export class Login {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
await this.bot.utils.wait(500)
|
||||
|
||||
const usedTotp = await this.tryAutoTotp(page, '2FA initial step')
|
||||
const usedTotp = await this.tryAutoTotp(page, '2FA initial step')
|
||||
if (usedTotp) return
|
||||
|
||||
const number = await this.fetchAuthenticatorNumber(page)
|
||||
const number = await this.fetchAuthenticatorNumber(page)
|
||||
if (number) { await this.approveAuthenticator(page, number); return }
|
||||
await this.handleSMSOrTotp(page)
|
||||
} catch (e) {
|
||||
@@ -686,7 +686,7 @@ export class Login {
|
||||
if (this.bot.config.parallel) {
|
||||
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
|
||||
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
|
||||
await this.bot.utils.wait(60000)
|
||||
await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile))
|
||||
@@ -710,14 +710,14 @@ export class Login {
|
||||
return
|
||||
} catch {
|
||||
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))
|
||||
const refreshed = await this.fetchAuthenticatorNumber(page)
|
||||
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
|
||||
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) {
|
||||
@@ -777,7 +777,7 @@ export class Login {
|
||||
}
|
||||
|
||||
private async ensureTotpInput(page: Page): Promise<string | null> {
|
||||
const selector = await this.findFirstTotpInput(page)
|
||||
const selector = await this.findFirstTotpInput(page)
|
||||
if (selector) return selector
|
||||
|
||||
const attempts = 4
|
||||
@@ -809,7 +809,7 @@ export class Login {
|
||||
try {
|
||||
const code = generateTOTP(this.currentTotpSecret!.trim())
|
||||
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')
|
||||
return
|
||||
}
|
||||
@@ -953,21 +953,21 @@ export class Login {
|
||||
if (el && el.textContent) texts.push(el.textContent)
|
||||
})
|
||||
return texts.join(' ')
|
||||
}).catch(()=>'')
|
||||
}).catch(() => '')
|
||||
|
||||
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
|
||||
} catch {/* fall through to false */}
|
||||
} catch {/* fall through to false */ }
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private async detectTotpHeading(page: Page): Promise<string | null> {
|
||||
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)
|
||||
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
|
||||
const lowered = text.toLowerCase()
|
||||
if (/authenticator/.test(lowered) && /code/.test(lowered)) return text
|
||||
@@ -1042,7 +1042,7 @@ export class Login {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
return false
|
||||
}).catch(() => false),
|
||||
@@ -1052,7 +1052,7 @@ export class Login {
|
||||
try {
|
||||
const el = document.querySelector(sel)
|
||||
if (el && (el as HTMLElement).offsetParent !== null) return true
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
return false
|
||||
}).catch(() => false)
|
||||
@@ -1072,7 +1072,7 @@ export class Login {
|
||||
if (checkCount % 2 === 0) { // Every other iteration
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(()=>false)) {
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
return sel
|
||||
}
|
||||
}
|
||||
@@ -1182,8 +1182,8 @@ export class Login {
|
||||
}
|
||||
}).catch(() => ({ title: 'unknown', bodyLength: 0, hasRewardsText: false, visibleElements: 0 }))
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Page info: ${JSON.stringify(pageContent)}`, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal element missing', 'error')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Page info: ${JSON.stringify(pageContent)}`, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal element missing', 'error')
|
||||
throw new Error(`Rewards portal not detected. URL: ${currentUrl}. Check reports/ folder`)
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Portal found via fallback (${fallbackSelector})`)
|
||||
@@ -1236,8 +1236,8 @@ export class Login {
|
||||
await this.bot.utils.wait(DEFAULT_TIMEOUTS.medium)
|
||||
const content = await page.content().catch(() => '')
|
||||
const hasHttp400 = content.includes('HTTP ERROR 400') ||
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
content.includes('This page isn\'t working') ||
|
||||
content.includes('This page is not working')
|
||||
|
||||
if (hasHttp400) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'HTTP 400 detected during Bing verification, reloading...', 'warn')
|
||||
@@ -1295,9 +1295,9 @@ export class Login {
|
||||
}
|
||||
|
||||
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) {
|
||||
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')
|
||||
}
|
||||
}
|
||||
@@ -1307,7 +1307,7 @@ export class Login {
|
||||
let did = false
|
||||
|
||||
// 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) {
|
||||
const text = (await skipBtn.textContent() || '').trim()
|
||||
// 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)
|
||||
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) {
|
||||
const btn = await page.$(SELECTORS.passkeySecondary)
|
||||
if (btn) {
|
||||
@@ -1333,18 +1333,18 @@ export class Login {
|
||||
|
||||
// Priority 3: Title + secondary button detection
|
||||
if (!did) {
|
||||
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { 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 titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { 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 title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
|
||||
const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title)
|
||||
if (looksLike && secBtn) {
|
||||
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile))
|
||||
did = true
|
||||
this.logPasskeyOnce('title heuristic '+title)
|
||||
this.logPasskeyOnce('title heuristic ' + title)
|
||||
}
|
||||
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)) {
|
||||
await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile))
|
||||
did = true
|
||||
@@ -1356,7 +1356,7 @@ export class Login {
|
||||
// Priority 4: XPath fallback (includes Windows Hello specific patterns)
|
||||
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()
|
||||
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))
|
||||
did = true
|
||||
this.logPasskeyOnce('xpath fallback')
|
||||
@@ -1399,13 +1399,13 @@ export class Login {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const yes = await page.$(SELECTORS.passkeyPrimary)
|
||||
if (yes) {
|
||||
await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile))
|
||||
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()
|
||||
if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) {
|
||||
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
|
||||
}
|
||||
} else if (did) {
|
||||
@@ -1425,7 +1425,7 @@ export class Login {
|
||||
private logPasskeyOnce(reason: string) {
|
||||
if (this.passkeyHandled) return
|
||||
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 ---------------
|
||||
@@ -1433,11 +1433,11 @@ export class Login {
|
||||
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'sign-in-blocked') return true
|
||||
try {
|
||||
let text = ''
|
||||
for (const sel of ['[data-testid="title"]','h1','div[role="heading"]','div.text-title']) {
|
||||
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(()=>null)
|
||||
for (const sel of ['[data-testid="title"]', 'h1', 'div[role="heading"]', 'div.text-title']) {
|
||||
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(() => null)
|
||||
if (el) {
|
||||
const t = (await el.textContent()||'').trim()
|
||||
if (t && t.length < 300) text += ' '+t
|
||||
const t = (await el.textContent() || '').trim()
|
||||
if (t && t.length < 300) text += ' ' + t
|
||||
}
|
||||
}
|
||||
const lower = text.toLowerCase()
|
||||
@@ -1453,7 +1453,7 @@ export class Login {
|
||||
next: ['Manual recovery required before continuing'],
|
||||
docsUrl
|
||||
}
|
||||
await this.sendIncidentAlert(incident,'warn')
|
||||
await this.sendIncidentAlert(incident, 'warn')
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'sign-in-blocked'
|
||||
this.startCompromisedInterval()
|
||||
@@ -1477,32 +1477,32 @@ export class Login {
|
||||
const recoveryEmail: string | undefined = this.bot.currentAccountRecoveryEmail
|
||||
if (!recoveryEmail || !/@/.test(recoveryEmail)) return
|
||||
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 refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r=>r.domain && r.prefix2)
|
||||
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)
|
||||
if (refs.length === 0) return
|
||||
|
||||
const candidates: string[] = []
|
||||
// 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 el = await page.waitForSelector(sel, { timeout: 1500 }).catch(()=>null)
|
||||
if (el) { const t = (await el.textContent()||'').trim(); if (t) candidates.push(t) }
|
||||
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(() => null)
|
||||
if (el) { const t = (await el.textContent() || '').trim(); if (t) candidates.push(t) }
|
||||
|
||||
// List items
|
||||
const li = page.locator('[role="listitem"], li')
|
||||
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) }
|
||||
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) }
|
||||
|
||||
// XPath generic masked patterns
|
||||
const xp = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]')
|
||||
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) }
|
||||
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) }
|
||||
|
||||
// Normalize
|
||||
const seen = new Set<string>()
|
||||
const norm = (s:string)=>s.replace(/\s+/g,' ').trim()
|
||||
const uniq = candidates.map(norm).filter(t=>t && !seen.has(t) && seen.add(t))
|
||||
const norm = (s: string) => s.replace(/\s+/g, ' ').trim()
|
||||
const uniq = candidates.map(norm).filter(t => t && !seen.has(t) && seen.add(t))
|
||||
// Masked filter
|
||||
let masked = uniq.filter(t=>/@/.test(t) && /[*•]/.test(t))
|
||||
let masked = uniq.filter(t => /@/.test(t) && /[*•]/.test(t))
|
||||
|
||||
if (masked.length === 0) {
|
||||
// Fallback full HTML scan
|
||||
@@ -1513,14 +1513,14 @@ export class Login {
|
||||
const found = new Set<string>()
|
||||
let m: RegExpExecArray | null
|
||||
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)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
if (masked.length === 0) return
|
||||
|
||||
// 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).
|
||||
// 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.
|
||||
@@ -1531,15 +1531,15 @@ export class Login {
|
||||
const use = m || loose
|
||||
const extracted = use ? use[0] : preferred
|
||||
const extractedLower = extracted.toLowerCase()
|
||||
let observedPrefix = ((use && use[1]) ? use[1] : '').toLowerCase()
|
||||
let observedDomain = ((use && use[2]) ? use[2] : '').toLowerCase()
|
||||
let observedPrefix = ((use && use[1]) ? use[1] : '').toLowerCase()
|
||||
let observedDomain = ((use && use[2]) ? use[2] : '').toLowerCase()
|
||||
if (!observedDomain && extractedLower.includes('@')) {
|
||||
const parts = extractedLower.split('@')
|
||||
observedDomain = parts[1] || ''
|
||||
}
|
||||
if (!observedPrefix && extractedLower.includes('@')) {
|
||||
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
|
||||
@@ -1555,22 +1555,22 @@ export class Login {
|
||||
if (!matchRef) {
|
||||
const docsUrl = this.getDocsUrl('recovery-email-mismatch')
|
||||
const incident: SecurityIncident = {
|
||||
kind:'Recovery email mismatch',
|
||||
kind: 'Recovery email mismatch',
|
||||
account: email,
|
||||
details:[
|
||||
details: [
|
||||
`MaskedShown: ${preferred}`,
|
||||
`Extracted: ${extracted}`,
|
||||
`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).',
|
||||
'Verify account security & recovery email in Microsoft settings.',
|
||||
'Update accounts.json if the change was legitimate before restart.'
|
||||
],
|
||||
docsUrl
|
||||
}
|
||||
await this.sendIncidentAlert(incident,'critical')
|
||||
await this.sendIncidentAlert(incident, 'critical')
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'recovery-mismatch'
|
||||
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))
|
||||
} else {
|
||||
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) {
|
||||
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()
|
||||
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 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 ---------------
|
||||
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn'|'critical'='warn') {
|
||||
const lines = [ `[Incident] ${incident.kind}`, `Account: ${incident.account}` ]
|
||||
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn' | 'critical' = 'warn') {
|
||||
const lines = [`[Incident] ${incident.kind}`, `Account: ${incident.account}`]
|
||||
if (incident.details?.length) lines.push(`Details: ${incident.details.join(' | ')}`)
|
||||
if (incident.next?.length) lines.push(`Next: ${incident.next.join(' -> ')}`)
|
||||
if (incident.docsUrl) lines.push(`Docs: ${incident.docsUrl}`)
|
||||
const level: 'warn'|'error' = severity === 'critical' ? 'error' : 'warn'
|
||||
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level)
|
||||
const level: 'warn' | 'error' = severity === 'critical' ? 'error' : 'warn'
|
||||
this.bot.log(this.bot.isMobile, 'SECURITY', lines.join(' | '), level)
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('../util/notifications/ConclusionWebhook')
|
||||
const fields = [
|
||||
{ name: 'Account', value: incident.account },
|
||||
...(incident.details?.length ? [{ name: 'Details', value: incident.details.join('\n') }] : []),
|
||||
@@ -1617,14 +1617,14 @@ export class Login {
|
||||
fields,
|
||||
severity === 'critical' ? 0xFF0000 : 0xFFAA00
|
||||
)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
|
||||
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 map: Record<string,string> = {
|
||||
'recovery-email-mismatch':'#recovery-email-mismatch',
|
||||
'we-cant-sign-you-in':'#we-cant-sign-you-in-blocked'
|
||||
const map: Record<string, string> = {
|
||||
'recovery-email-mismatch': '#recovery-email-mismatch',
|
||||
'we-cant-sign-you-in': '#we-cant-sign-you-in-blocked'
|
||||
}
|
||||
return anchor && map[anchor] ? `${base}${map[anchor]}` : base
|
||||
}
|
||||
@@ -1635,9 +1635,9 @@ export class Login {
|
||||
clearInterval(this.compromisedInterval)
|
||||
this.compromisedInterval = undefined
|
||||
}
|
||||
this.compromisedInterval = setInterval(()=>{
|
||||
this.compromisedInterval = setInterval(() => {
|
||||
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 {
|
||||
// Intentionally silent: If logging fails in interval, don't crash the timer
|
||||
// The interval will try again in 5 minutes
|
||||
@@ -1657,7 +1657,7 @@ export class Login {
|
||||
const ctx = page.context()
|
||||
const tab = await ctx.newPage()
|
||||
await tab.goto(url, { waitUntil: 'domcontentloaded' })
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
|
||||
// --------------- Infrastructure ---------------
|
||||
|
||||
@@ -4,23 +4,23 @@ import { TIMEOUTS } from '../constants'
|
||||
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
|
||||
import JobState from '../util/JobState'
|
||||
import { logError } from '../util/Logger'
|
||||
import { Retry } from '../util/Retry'
|
||||
import { Retry } from '../util/core/Retry'
|
||||
import { AdaptiveThrottler } from '../util/notifications/AdaptiveThrottler'
|
||||
import { logError } from '../util/notifications/Logger'
|
||||
import JobState from '../util/state/JobState'
|
||||
|
||||
// Selector patterns (extracted to avoid magic strings)
|
||||
const ACTIVITY_SELECTORS = {
|
||||
byName: (name: string) => `[data-bi-id^="${name}"] .pointLink:not(.contentContainer .pointLink)`,
|
||||
byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .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)`
|
||||
} as const
|
||||
|
||||
// Activity processing delays (in milliseconds)
|
||||
const ACTIVITY_DELAYS = {
|
||||
THROTTLE_MIN: 800,
|
||||
THROTTLE_MAX: 1400,
|
||||
ACTIVITY_SPACING_MIN: 1200,
|
||||
ACTIVITY_SPACING_MAX: 2600
|
||||
THROTTLE_MIN: 800,
|
||||
THROTTLE_MAX: 1400,
|
||||
ACTIVITY_SPACING_MIN: 1200,
|
||||
ACTIVITY_SPACING_MAX: 2600
|
||||
} as const
|
||||
|
||||
export class Workers {
|
||||
|
||||
24
src/index.ts
24
src/index.ts
@@ -7,16 +7,16 @@ import type { Page } from 'playwright'
|
||||
import { createInterface } from 'readline'
|
||||
import BrowserFunc from './browser/BrowserFunc'
|
||||
import BrowserUtil from './browser/BrowserUtil'
|
||||
import Axios from './util/Axios'
|
||||
import { detectBanReason } from './util/BanDetector'
|
||||
import Humanizer from './util/Humanizer'
|
||||
import JobState from './util/JobState'
|
||||
import { loadAccounts, loadConfig } from './util/Load'
|
||||
import { log } from './util/Logger'
|
||||
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
||||
import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
||||
import { StartupValidator } from './util/StartupValidator'
|
||||
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/Utils'
|
||||
import Humanizer from './util/browser/Humanizer'
|
||||
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils'
|
||||
import Axios from './util/network/Axios'
|
||||
import { QueryDiversityEngine } from './util/network/QueryDiversityEngine'
|
||||
import { log } from './util/notifications/Logger'
|
||||
import JobState from './util/state/JobState'
|
||||
import { loadAccounts, loadConfig } from './util/state/Load'
|
||||
import { MobileRetryTracker } from './util/state/MobileRetryTracker'
|
||||
import { detectBanReason } from './util/validation/BanDetector'
|
||||
import { StartupValidator } from './util/validation/StartupValidator'
|
||||
|
||||
import { Activities } from './functions/Activities'
|
||||
import { Login } from './functions/Login'
|
||||
@@ -629,7 +629,7 @@ export class MicrosoftRewardsBot {
|
||||
try {
|
||||
const h = this.config?.humanization
|
||||
if (!h || h.immediateBanAlert === false) return
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🚫 Ban Detected',
|
||||
@@ -806,7 +806,7 @@ export class MicrosoftRewardsBot {
|
||||
/** Send a strong alert to all channels and mention @everyone when entering global security standby. */
|
||||
private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> {
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🚨 Critical Security Alert',
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContext } from 'rebrowser-playwright'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { AccountProxy } from '../interface/Account'
|
||||
import type { MicrosoftRewardsBot } from '../../index'
|
||||
import type { AccountProxy } from '../../interface/Account'
|
||||
|
||||
/**
|
||||
* Create a browser instance for the given account
|
||||
@@ -26,7 +26,7 @@ export async function createBrowserInstance(
|
||||
proxy: AccountProxy,
|
||||
email: string
|
||||
): Promise<BrowserContext> {
|
||||
const browserModule = await import('../browser/Browser')
|
||||
const browserModule = await import('../../browser/Browser')
|
||||
const Browser = browserModule.default
|
||||
const browserInstance = new Browser(bot)
|
||||
return await browserInstance.createBrowser(proxy, email)
|
||||
@@ -1,6 +1,6 @@
|
||||
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 {
|
||||
private util: Util
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../interface/UserAgentUtil'
|
||||
import { log } from './Logger'
|
||||
import { Retry } from './Retry'
|
||||
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../../interface/UserAgentUtil'
|
||||
import { Retry } from '../core/Retry'
|
||||
import { log } from '../notifications/Logger'
|
||||
|
||||
interface UserAgentMetadata {
|
||||
mobile: boolean
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ConfigRetryPolicy } from '../interface/Config'
|
||||
import type { ConfigRetryPolicy } from '../../interface/Config'
|
||||
import { Util } from './Utils'
|
||||
|
||||
type NumericPolicy = {
|
||||
@@ -2,7 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } f
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
import { AccountProxy } from '../../interface/Account'
|
||||
|
||||
class AxiosClient {
|
||||
private instance: AxiosInstance
|
||||
@@ -142,10 +142,10 @@ class AxiosClient {
|
||||
|
||||
const code = e.code || e.cause?.code
|
||||
const isNetworkError = code === 'ECONNREFUSED' ||
|
||||
code === 'ETIMEDOUT' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'ENOTFOUND' ||
|
||||
code === 'EPIPE'
|
||||
code === 'ETIMEDOUT' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'ENOTFOUND' ||
|
||||
code === 'EPIPE'
|
||||
|
||||
const msg = String(e.message || '')
|
||||
const isProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { Util } from './Utils'
|
||||
import { Util } from '../core/Utils'
|
||||
|
||||
export interface QueryDiversityConfig {
|
||||
sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import { Config } from '../interface/Config'
|
||||
import { Ntfy } from './Ntfy'
|
||||
import { DISCORD } from '../../constants'
|
||||
import { Config } from '../../interface/Config'
|
||||
import { log } from './Logger'
|
||||
import { DISCORD } from '../constants'
|
||||
import { Ntfy } from './Ntfy'
|
||||
|
||||
interface DiscordField {
|
||||
name: string
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import { DISCORD } from '../constants'
|
||||
import { Config } from '../interface/Config'
|
||||
import { DISCORD } from '../../constants'
|
||||
import { Config } from '../../interface/Config'
|
||||
|
||||
interface ErrorReportPayload {
|
||||
error: string
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
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 { loadConfig } from './Load'
|
||||
import { Ntfy } from './Ntfy'
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loadConfig } from './Load'
|
||||
import axios from 'axios'
|
||||
import { loadConfig } from '../state/Load'
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
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 path from 'path'
|
||||
import type { Config } from '../interface/Config'
|
||||
import type { Config } from '../../interface/Config'
|
||||
|
||||
type AccountCompletionMeta = {
|
||||
runId?: string
|
||||
@@ -2,9 +2,9 @@ import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { BrowserContext, Cookie } from 'rebrowser-playwright'
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../interface/Config'
|
||||
import { Util } from './Utils'
|
||||
import { Account } from '../../interface/Account'
|
||||
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../../interface/Config'
|
||||
import { Util } from '../core/Utils'
|
||||
|
||||
const utils = new Util()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import chalk from 'chalk'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config } from '../interface/Config'
|
||||
import { log } from './Logger'
|
||||
import { Account } from '../../interface/Account'
|
||||
import { Config } from '../../interface/Config'
|
||||
import { log } from '../notifications/Logger'
|
||||
|
||||
interface ValidationError {
|
||||
severity: 'error' | 'warning'
|
||||
@@ -181,12 +181,12 @@ export class StartupValidator {
|
||||
private validateConfig(config: Config): void {
|
||||
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
|
||||
if (maybeSchedule !== undefined) {
|
||||
this.addWarning(
|
||||
'config',
|
||||
'Legacy schedule settings detected in config.jsonc.',
|
||||
'Remove schedule.* entries and use your operating system scheduler.',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
this.addWarning(
|
||||
'config',
|
||||
'Legacy schedule settings detected in config.jsonc.',
|
||||
'Remove schedule.* entries and use your operating system scheduler.',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
// Headless mode in Docker
|
||||
@@ -431,8 +431,8 @@ export class StartupValidator {
|
||||
|
||||
// Check if at least one worker is enabled
|
||||
const anyEnabled = workers.doDailySet || workers.doMorePromotions || workers.doPunchCards ||
|
||||
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
|
||||
workers.doReadToEarn
|
||||
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
|
||||
workers.doReadToEarn
|
||||
|
||||
if (!anyEnabled) {
|
||||
this.addWarning(
|
||||
Reference in New Issue
Block a user