From e981a69095a1966e34da5bbc4a0c7beaa980fdb9 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Fri, 2 Jan 2026 18:38:56 +0100 Subject: [PATCH] Refactor bot startup process to auto-create configuration files, enhance update system, and add historical stats endpoints - Updated README.md to reflect new bot setup and configuration process. - Removed outdated installer README and integrated update logic directly into the bot. - Implemented smart update for example configuration files, ensuring user files are not overwritten. - Added FileBootstrap class to handle automatic creation of configuration files on first run. - Enhanced BotController to manage stop requests and ensure graceful shutdown. - Introduced new stats management features, including historical stats and activity breakdown endpoints. - Updated API routes to include new statistics retrieval functionalities. --- .github/copilot-instructions.md | 1473 +++++++------------------------ README.md | 10 +- scripts/installer/README.md | 176 ---- scripts/installer/update.mjs | 264 ++++-- src/config.example.jsonc | 4 +- src/dashboard/BotController.ts | 67 +- src/dashboard/StatsManager.ts | 62 ++ src/dashboard/routes.ts | 36 +- src/index.ts | 16 + src/util/core/FileBootstrap.ts | 116 +++ 10 files changed, 816 insertions(+), 1408 deletions(-) delete mode 100644 scripts/installer/README.md create mode 100644 src/util/core/FileBootstrap.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1e4251b..d087962 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,1200 +1,389 @@ ---- -description: Comprehensive development, architecture, and workflow rules for Microsoft Rewards Bot -applyTo: "**" ---- +# Copilot Instructions - Microsoft Rewards Bot -# Microsoft Rewards Bot - GitHub Copilot Instructions +## Project Architecture -## πŸ“‹ Project Overview - -**Microsoft Rewards Bot** is a TypeScript-based automation tool that earns Microsoft Rewards points by completing daily tasks, searches, quizzes, and promotional activities. The bot emphasizes **human-like behavior**, **anti-detection measures**, and **long-term reliability**. - -### Key Technologies -- **Language:** TypeScript (strict mode, ES2020 target) -- **Browser Automation:** Playwright + rebrowser-playwright (anti-detection) -- **Fingerprinting:** fingerprint-generator + fingerprint-injector -- **Runtime:** Node.js 20+ (22 recommended) -- **Build System:** TypeScript compiler (tsc) -- **Testing:** Node.js native test runner -- **Deployment:** Docker + Docker Compose support - -### Project Structure +### Core Structure ``` src/ -β”œβ”€β”€ index.ts # Main entry point + bot orchestration -β”œβ”€β”€ config.jsonc # User configuration (NOT committed) -β”œβ”€β”€ accounts.jsonc # User accounts (NOT committed, sensitive) -β”œβ”€β”€ constants.ts # Global constants (timeouts, URLs, colors) -β”œβ”€β”€ browser/ # Browser initialization + utilities -β”‚ β”œβ”€β”€ Browser.ts # Playwright browser factory -β”‚ β”œβ”€β”€ BrowserFunc.ts # Navigation, dashboard scraping -β”‚ └── BrowserUtil.ts # Tab management, humanization -β”œβ”€β”€ flows/ # High-level automation workflows -β”‚ β”œβ”€β”€ DesktopFlow.ts # Desktop automation sequence -β”‚ β”œβ”€β”€ MobileFlow.ts # Mobile automation sequence -β”‚ β”œβ”€β”€ FlowUtils.ts # Shared flow utilities -β”‚ └── SummaryReporter.ts # Run reporting + webhooks -β”œβ”€β”€ functions/ # Core activity handlers -β”‚ β”œβ”€β”€ Activities.ts # Activity type router -β”‚ β”œβ”€β”€ Login.ts # Microsoft account authentication -β”‚ β”œβ”€β”€ Workers.ts # Task execution (DailySet, Punch Cards, etc.) -β”‚ └── activities/ # Individual activity implementations -β”‚ β”œβ”€β”€ Search.ts # Desktop/Mobile search automation -β”‚ β”œβ”€β”€ Quiz.ts # Quiz solving logic -β”‚ β”œβ”€β”€ Poll.ts # Poll completion -β”‚ β”œβ”€β”€ ThisOrThat.ts # This or That game -β”‚ └── ... -β”œβ”€β”€ 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 -β”‚ β”œβ”€β”€ BotController.ts # Bot lifecycle management -β”‚ β”œβ”€β”€ state.ts # Shared dashboard state -β”‚ └── SessionLoader.ts # Account session management -β”œβ”€β”€ interface/ # TypeScript interfaces -β”‚ β”œβ”€β”€ Account.ts # Account structure (email, password, totp, proxy) -β”‚ β”œβ”€β”€ Config.ts # Configuration schema -β”‚ β”œβ”€β”€ DashboardData.ts # Microsoft Rewards dashboard API types -β”‚ └── ... -└── account-creation/ # Account creation automation - β”œβ”€β”€ cli.ts # CLI interface - β”œβ”€β”€ AccountCreator.ts # Microsoft account registration (2671 LINES!) - β”œβ”€β”€ DataGenerator.ts # Realistic user data generation - β”œβ”€β”€ 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/ # Automation scripts -└── installer/ # Setup and update automation - β”œβ”€β”€ setup.mjs # Initial setup automation - β”œβ”€β”€ update.mjs # GitHub ZIP-based auto-updater (NO GIT REQUIRED!) - └── README.md # Installer documentation -setup/ # Setup and execution scripts -β”œβ”€β”€ setup.bat # Windows setup script -β”œβ”€β”€ setup.sh # Linux/Mac setup script -β”œβ”€β”€ run.sh # Nix development environment launcher -β”œβ”€β”€ nix/ # NixOS configuration -β”‚ β”œβ”€β”€ flake.nix # Nix flake definition -β”‚ └── flake.lock # Nix flake lock file -└── README.md # Setup guide +β”œβ”€β”€ index.ts # Main entry point, bot orchestration +β”œβ”€β”€ constants.ts # Global constants and configurations +β”œβ”€β”€ account-creation/ # Automated account creation system +β”œβ”€β”€ browser/ # Browser automation (Playwright) +β”œβ”€β”€ dashboard/ # Web dashboard (Express + WebSockets) +β”œβ”€β”€ flows/ # Desktop/Mobile automation flows +β”œβ”€β”€ functions/ # Core features (Login, Activities, Workers) +β”œβ”€β”€ interface/ # TypeScript interfaces and types +β”œβ”€β”€ scheduler/ # Task scheduling system +└── util/ # Utility modules + β”œβ”€β”€ browser/ # Browser utilities (UA, Humanizer) + β”œβ”€β”€ core/ # Core utilities (Utils, MemoryMonitor) + β”œβ”€β”€ network/ # HTTP clients, query engines + β”œβ”€β”€ notifications/ # Discord, NTFY notifications + β”œβ”€β”€ security/ # Anti-detection, ban detection + β”œβ”€β”€ state/ # State management (JobState, Load) + └── validation/ # Input validation, startup checks ``` ---- +### Key Design Patterns -## πŸ—οΈ Architecture Principles +#### 1. **Bot Class Pattern** (`index.ts`) +- Main orchestrator with dependency injection +- Manages lifecycle: initialize β†’ run β†’ cleanup +- Supports cluster mode (multi-account parallel processing) +- Implements crash recovery and graceful shutdown -### 1. Separation of Concerns -- **Flows** orchestrate high-level workflows (Desktop/Mobile sequences) -- **Functions** implement core reusable tasks (login, search, activities) -- **Utils** provide infrastructure (logging, retries, humanization) -- **Browser** handles all Playwright interactions (initialization, navigation, scraping) +#### 2. **Flow Pattern** (`flows/`) +- **DesktopFlow**: Desktop browser automation +- **MobileFlow**: Mobile browser automation +- **SummaryReporter**: Centralized reporting system +- Each flow is self-contained with error handling -### 2. Single Responsibility -- Each class/module has **one clear purpose** -- No "god classes" - split complex logic into focused modules -- Example: `BrowserFactory.ts` centralizes browser creation (was duplicated in flows) +#### 3. **Activity Handler Pattern** (`functions/activities/`) +- Each activity (Quiz, Poll, Search, etc.) is a separate class +- Implements error recovery and retry logic +- Uses human-like delays and behaviors -### 3. Error Handling Strategy -- **All async operations MUST have try-catch blocks** -- Use `Retry` utility for transient failures (network, timeouts) -- Use `BanDetector` to identify security-related errors (CAPTCHA, lockouts) -- Log errors with context using `formatDetailedError()` utility -- Never swallow errors silently - always log or propagate +#### 4. **State Management** (`util/state/`) +- **JobState**: Persistent task completion tracking +- **AccountHistory**: Historical data storage +- **Load**: Configuration and account loading +- Files use `.jsonc` format (JSON with comments) -### 4. Type Safety -- **Strict TypeScript mode** enforced via `tsconfig.json` -- No `any` types - use proper types or `unknown` + type guards -- All public APIs MUST have explicit return types -- Use interface type guards for runtime validation (e.g., `isWorkerMessage()`) - -### 5. State Management -- Bot state lives in `MicrosoftRewardsBot` class instance -- Job state (completed tasks) persists to disk via `JobState` utility -- Dashboard state managed via `dashboardState` singleton -- No global mutable state outside these systems +#### 5. **Dashboard Architecture** (`dashboard/`) +- **Server**: Express server with WebSockets +- **BotController**: Bot lifecycle management for dashboard +- **StatsManager**: Persistent statistics tracking +- **State**: In-memory state management +- Real-time updates via WebSocket broadcast --- -## πŸ› οΈ Development Workflow +## Critical Implementation Rules -### Build & Run Commands -```bash -# Install dependencies -npm install +### 1. **Configuration Files** -# Build TypeScript β†’ JavaScript -npm run build +**IMPORTANT**: Files use `.example.jsonc` templates: +- `src/config.example.jsonc` β†’ `src/config.jsonc` +- `src/accounts.example.jsonc` β†’ `src/accounts.jsonc` +- Auto-copied on first run by `FileBootstrap.ts` -# Run the bot (production) -npm start - -# Development mode (hot reload) -npm run dev - -# Run tests -npm run test - -# Type checking only -npm run typecheck - -# Dashboard mode (standalone) -npm run dashboard - -# Account creator -npm run creator -``` - -### Critical: Always Build Before Running -- Users run `npm install` β†’ installs dependencies -- Users run `npm start` β†’ executes `dist/index.js` (compiled output) -- **NEVER suggest running source files directly** unless in dev mode -- The `prestart` script auto-builds if `dist/` is missing - -### Verification Steps After Changes -1. **Type Check:** `npm run typecheck` (must pass with 0 errors) -2. **Build:** `npm run build` (must succeed) -3. **Test:** `npm run test` (if tests exist for changed code) -4. **Manual Test:** Run bot with 1 test account to verify behavior - ---- - -## πŸ“œ Code Style & Standards - -### Language & Formatting -- **Language:** All code, comments, and documentation in **English only** -- **Naming Conventions:** - - Variables/Functions: `camelCase` (e.g., `getCurrentPoints`, `isMobile`) - - Classes: `PascalCase` (e.g., `MicrosoftRewardsBot`, `BrowserFactory`) - - Constants: `UPPER_SNAKE_CASE` (e.g., `TIMEOUTS.DASHBOARD_WAIT`) - - Private methods: prefix with `private` keyword (e.g., `private handleError()`) -- **Indentation:** 4 spaces (configured in `tsconfig.json`) -- **Quotes:** Double quotes for strings (`"hello"`) -- **Semicolons:** Always use semicolons - -### Function & Class Design -- **Functions:** - - Keep functions **small and focused** (max ~50 lines) - - Extract complex logic into helper functions - - Use descriptive names that explain behavior (e.g., `shouldSkipAccount()`, not `check()`) - - Add JSDoc comments for public APIs -- **Classes:** - - Constructor should initialize state only (no heavy logic) - - Keep public API surface minimal - - Use private methods for internal logic - - Inject dependencies via constructor (e.g., `bot` instance) - -### Error Handling Patterns +**Loading Order**: +```typescript +// 1. Check multiple locations +const candidates = [ + 'src/config.jsonc', + 'config.jsonc', + 'src/config.json' // Legacy support +] + +// 2. Strip JSON comments +const text = stripJsonComments(rawContent) + +// 3. Normalize schema (flat + nested support) +const config = normalizeConfig(JSON.parse(text)) +``` + +### 2. **Browser Automation** + +**Always use these utilities**: +```typescript +// Humanized typing +await this.browser.utils.humanType(page, selector, text) + +// Smart waits (handles stale elements) +await this.browser.utils.smartWait(page, selector, { timeout: 30000 }) + +// Random gestures (anti-detection) +await this.humanizer.randomGesture(page) +``` + +**NEVER**: +- Direct `page.type()` or `page.fill()` +- Fixed delays (`await page.waitForTimeout(5000)`) +- Hardcoded selectors without fallback + +### 3. **Error Handling** + +**Pattern to follow**: ```typescript -// βœ… GOOD: Explicit error handling with context try { - await page.click(selector) + // Attempt operation + await riskyOperation() } catch (error) { - this.bot.log(this.bot.isMobile, 'ACTIVITY', `Failed to click: ${selector}`, 'error') - throw new Error(`Activity click failed: ${getErrorMessage(error)}`) + // 1. Detect ban/security + const ban = detectBanReason(error) + if (ban.status) { + await this.engageGlobalStandby(ban.reason, email) + throw error + } + + // 2. Log with context + log(this.isMobile, 'MODULE', `Failed: ${getErrorMessage(error)}`, 'error') + + // 3. Retry or fail gracefully + if (retryCount < MAX_RETRIES) { + await this.utils.wait(BACKOFF_MS) + return await retryOperation() + } + + throw error +} +``` + +### 4. **Logging System** + +**Use centralized logger**: +```typescript +import { log } from '../util/notifications/Logger' + +// Format: log(isMobile, source, message, level, color) +log(false, 'SEARCH', 'Starting desktop searches', 'log', 'cyan') +log(true, 'QUIZ', 'Quiz failed: timeout', 'error', 'red') +``` + +**Levels**: `'log' | 'warn' | 'error'` +**Colors**: `'cyan' | 'yellow' | 'red' | 'green'` + +### 5. **State Persistence** + +**Always use JobState for idempotency**: +```typescript +// Check if already done +if (this.accountJobState.isCompleted(email, activityKey)) { + log(mobile, 'ACTIVITY', `${activityKey} already completed`, 'warn') + return } -// ❌ BAD: Silent failure -try { - await page.click(selector) -} catch { - // Ignoring error -} +// Mark as completed after success +this.accountJobState.markCompleted(email, activityKey) +``` -// βœ… GOOD: Using Retry utility for transient failures -const retry = new Retry(this.bot.config.retryPolicy) -await retry.run(async () => { - return await this.fetchData() -}, (error) => { - // Only retry on network errors - return error instanceof NetworkError +**Activity Keys**: +- `desktop-search`, `mobile-search` +- `daily-set`, `more-promotions`, `punch-cards` +- `read-to-earn`, `daily-check-in` + +### 6. **Dashboard Integration** + +**Update dashboard state**: +```typescript +import { dashboardState } from '../dashboard/state' + +// Update account status +dashboardState.updateAccount(email, { + status: 'running', // 'idle' | 'running' | 'completed' | 'error' + errors: [] +}) + +// Add logs +dashboardState.addLog({ + timestamp: new Date().toISOString(), + level: 'log', + platform: 'DESKTOP', + title: 'SEARCH', + message: 'Completed 30 searches' }) ``` -### Logging Standards +### 7. **Anti-Detection** + +**Mandatory humanization**: ```typescript -// βœ… GOOD: Contextual logging with severity -this.bot.log(this.bot.isMobile, 'SEARCH', `Starting desktop search with ${queries.length} queries`) -this.bot.log(this.bot.isMobile, 'SEARCH', `Search completed: +${pointsEarned} points`, 'log', 'green') -this.bot.log(this.bot.isMobile, 'SEARCH', `Network timeout on query "${query}"`, 'warn') +// Random delays between actions +await this.humanizer.randomDelay('search') // 2-5 seconds -// ❌ BAD: Generic console.log -console.log('Search done') +// Mouse movements +await this.humanizer.randomGesture(page) -// βœ… GOOD: Error logging with full context -this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity failed: ${formatDetailedError('quiz', error, verbose)}`, 'error') +// Scroll simulation +await this.browser.utils.scrollRandomAmount(page) + +// Click with human-like coordinates +await this.browser.utils.humanClick(page, selector) ``` ---- - -## πŸ”’ Security & Privacy - -### Sensitive Data Handling -- **NEVER log passwords, TOTP secrets, or recovery codes** -- Email redaction is automatic via `logging.redactEmails` config -- Proxy credentials are sanitized in logs -- Session files (`sessions/`) contain sensitive cookies - never commit -- `accounts.jsonc` and `config.jsonc` are **gitignored** (user data) - -### Anti-Detection Measures -- **Humanization MUST always be enabled** (`humanization.enabled: true`) -- Use random delays between actions (configured via `Humanizer`) -- Browser fingerprinting via `fingerprint-generator` (masks automation) -- Natural search patterns via `QueryDiversityEngine` (Google Trends, Reddit) -- Mouse gestures and scrolling via `BrowserUtil.humanizePage()` - -### Ban Prevention -- **Never disable humanization** in production code -- Detect bans early via `BanDetector` (CAPTCHA, lockout messages) -- Implement `stopOnBan` to halt automation on first ban -- Use `globalStandby` mode to keep browser open for manual review -- Support proxy rotation per account +**CRITICAL**: Never disable humanization in production! --- -## πŸš€ Feature Development Guidelines +## Common Tasks -### Before Starting Any Feature -1. **Understand the full codebase context:** - - Read related modules in `src/flows/`, `src/functions/`, `src/util/` - - Check existing patterns (e.g., how other activities are implemented) - - Review interfaces in `src/interface/` -2. **Check configuration:** - - New features should have corresponding `config.jsonc` flags - - Default to safe/conservative behavior (e.g., `enabled: false`) -3. **Plan for long-term maintainability:** - - No quick patches - solve root cause - - Write reusable utilities, not one-off hacks - - Consider future extensibility +### Adding a New Activity -### Adding a New Activity Type -1. **Create activity handler in `src/functions/activities/`:** - ```typescript - // src/functions/activities/MyActivity.ts - import { Page } from 'rebrowser-playwright' - import { MicrosoftRewardsBot } from '../../index' - - export class MyActivity { - private bot: MicrosoftRewardsBot - - constructor(bot: MicrosoftRewardsBot) { - this.bot = bot - } - - async complete(page: Page): Promise { - this.bot.log(this.bot.isMobile, 'MY-ACTIVITY', 'Starting MyActivity') - // Implementation here - } - } - ``` - -2. **Register in `Activities.ts`:** - ```typescript - case 'myActivityType': - const myActivity = new MyActivity(this.bot) - await myActivity.complete(page) - break - ``` - -3. **Add config flag in `interface/Config.ts`:** - ```typescript - export interface Workers { - doMyActivity?: boolean - } - ``` - -4. **Update default config in `src/config.jsonc`:** - ```jsonc - "workers": { - "doMyActivity": true // Add with sensible default - } - ``` - -5. **Document in appropriate docs file** (e.g., `docs/activities.md`) - -### Adding a New Utility -1. **Place in `src/util/` with clear naming** -2. **Export as class or function (prefer functions for stateless utilities)** -3. **Add JSDoc comments for public API** -4. **Write unit tests in `tests/` directory** -5. **Update imports in dependent modules** - -### Modifying Core Logic -- **ALWAYS verify no regressions:** - - Run full test suite: `npm run test` - - Test with 1-2 real accounts before deploying - - Check logs for errors/warnings -- **Maintain backwards compatibility:** - - Don't break existing configs - - Support legacy config formats with deprecation warnings -- **Update documentation:** - - Update relevant `docs/*.md` files - - Update inline JSDoc comments - - Update README.md if user-facing - ---- - -## πŸ§ͺ Testing Standards - -### Test Structure -- **Location:** All tests in `tests/` directory (mirrors `src/` structure) -- **Naming:** `filename.test.ts` (e.g., `queryDiversityEngine.test.ts`) -- **Runner:** Node.js native test runner (`node --test`) - -### What to Test -- **Utilities:** Pure functions in `src/util/` (high priority) -- **Type Guards:** Runtime type validation functions -- **Complex Logic:** Ban detection, retry policies, query generation -- **Edge Cases:** Empty inputs, malformed data, timeouts - -### Example Test +1. **Create activity class**: ```typescript -// tests/util/utils.test.ts -import { describe, it } from 'node:test' -import assert from 'node:assert' -import { shortErrorMessage } from '../src/util/Utils' +// src/functions/activities/NewActivity.ts +import { ActivityHandler } from '../../interface/ActivityHandler' -describe('Utils', () => { - it('should extract error message from Error instance', () => { - const error = new Error('Test error') - const result = shortErrorMessage(error) - assert.strictEqual(result, 'Test error') - }) - - it('should handle unknown error types', () => { - const result = shortErrorMessage({ custom: 'object' }) - assert.strictEqual(result, '[object Object]') - }) -}) -``` - -### Running Tests -```bash -# Run all tests -npm run test - -# Run specific test file -node --test tests/util/utils.test.ts -``` - ---- - -## πŸ“¦ Dependencies & Updates - -### Core Dependencies -- **playwright** + **rebrowser-playwright:** Browser automation (must match versions) -- **fingerprint-generator** + **fingerprint-injector:** Anti-detection -- **axios:** HTTP client (with proxy support) -- **cheerio:** HTML parsing (dashboard scraping) -- **express** + **ws:** Dashboard server + WebSocket -- **chalk:** Console output colors - -### Dependency Management -- **NEVER add dependencies without justification** -- **Check if existing dependencies can solve the problem first** -- **Verify compatibility:** - - Check `package.json` engines field (Node.js 20+) - - Test on Windows and Linux - - Verify Docker compatibility -- **Update dependencies cautiously:** - - Playwright updates may break anti-detection - - Test thoroughly after updates - -### Adding a New Dependency -1. **Justify the need** (why can't existing code solve this?) -2. **Check package health:** - - Active maintenance - - No critical security vulnerabilities - - Compatible license (MIT, Apache, BSD) -3. **Install with exact version:** - ```bash - npm install --save-exact package-name@1.2.3 - ``` -4. **Update documentation:** - - Add to relevant section in README.md - - Document usage in code comments - ---- - -## πŸ› Debugging & Diagnostics - -### Debug Mode -```bash -# Enable verbose logging -DEBUG_REWARDS_VERBOSE=1 npm start - -# Force headless mode (useful for debugging in Docker) -FORCE_HEADLESS=1 npm start - -# Skip random sleep delays (faster testing) -SKIP_RANDOM_SLEEP=true npm start -``` - -### Common Issues & Solutions - -#### "Browser launch failed" -```bash -# Solution: Install Chromium -npx playwright install chromium -``` - -#### "Account credentials invalid" -- Check `accounts.jsonc` has correct email/password -- If 2FA enabled, verify `totp` field has correct secret -- Test manual login at https://login.live.com/ - -#### "Ban detected" -- Check `humanization.enabled: true` in config -- Review logs for CAPTCHA or security messages -- Use `BanDetector` output to identify root cause -- Consider proxies or reduced frequency - -#### "Module not found" -```bash -# Solution: Reinstall and rebuild -rm -rf node_modules dist -npm install -npm run build -``` - -### Logging Strategy -- **Console logs:** Real-time monitoring (user-facing) -- **Webhook logs:** Remote monitoring (Discord, Ntfy) -- **Job state:** Persistent completion tracking (resume after crash) -- **Error logs:** Detailed stack traces (verbose mode) - ---- - -## πŸ”„ Git Workflow & Versioning - -### Commit Message Format -Use **Conventional Commits** style: -``` -feat: Add support for new quiz type -fix: Resolve mobile search retry loop -refactor: Extract browser factory to centralized utility -docs: Update scheduling guide with cron examples -test: Add unit tests for QueryDiversityEngine -chore: Update dependencies to latest versions -``` - -### Branching Strategy -- **main:** Production-ready code (always stable) -- **feature/name:** New features (merge to main via PR) -- **fix/issue:** Bug fixes (merge to main via PR) -- **refactor/name:** Code improvements (no behavior change) - -### Pull Request Checklist -- [ ] Code follows style guide (English, camelCase, etc.) -- [ ] All tests pass (`npm run test`) -- [ ] Type checking passes (`npm run typecheck`) -- [ ] Build succeeds (`npm run build`) -- [ ] Tested manually with real account -- [ ] Documentation updated (README, docs/) -- [ ] No sensitive data in commits (no secrets, keys, emails) - ---- - -## πŸ“– Documentation Standards - -### Code Documentation -- **JSDoc comments for all public APIs:** - ```typescript - /** - * Execute the full desktop automation flow for an account - * - * Performs tasks in sequence: login, daily set, promotions, searches - * - * @param account Account to process (email, password, totp, proxy) - * @returns Promise resolving to points collected during the flow - * @throws {Error} If critical operation fails (login, browser init) - * - * @example - * ```typescript - * const flow = new DesktopFlow(bot) - * const result = await flow.run(account) - * ``` - */ - async run(account: Account): Promise - ``` - -- **Inline comments for complex logic:** - ```typescript - // IMPROVED: Use centralized browser factory to eliminate duplication - const browser = await createBrowserInstance(this.bot, account.proxy, account.email) - ``` - -- **Explain WHY, not WHAT:** - ```typescript - // ❌ BAD: Obvious comment - // Increment counter - counter++ - - // βœ… GOOD: Explains reasoning - // Retry mobile search due to known Microsoft API instability - await retry.run(() => doMobileSearch()) - ``` - -### User Documentation -- **Location:** All user-facing docs in `docs/` directory -- **Style:** Clear, concise, beginner-friendly -- **Structure:** - - Overview at top - - Table of contents for long docs - - Code examples with syntax highlighting - - Troubleshooting section - - Related links at bottom - -### When to Update Documentation -- **Always:** - - New config options β†’ Update `docs/config.md` or inline in `config.jsonc` - - New CLI commands β†’ Update `docs/commands.md` - - Behavior changes β†’ Update relevant guide -- **Never:** - - Minor bug fixes (no user-visible change) - - Refactoring (no API change) - - Internal implementation details - ---- - -## 🚫 Anti-Patterns & Common Mistakes - -### ❌ Don't Do This - -1. **Magic Numbers:** - ```typescript - // ❌ BAD - await page.waitForTimeout(5000) - - // βœ… GOOD - await page.waitForTimeout(TIMEOUTS.ACTIVITY_WAIT) - ``` - -2. **Hardcoded URLs:** - ```typescript - // ❌ BAD - await page.goto('https://rewards.bing.com') - - // βœ… GOOD - await this.bot.browser.func.goHome(page) - ``` - -3. **Silent Failures:** - ```typescript - // ❌ BAD - try { - await doSomething() - } catch { - // Ignore - } - - // βœ… GOOD - try { - await doSomething() - } catch (error) { - this.bot.log(this.bot.isMobile, 'TASK', `Operation failed: ${getErrorMessage(error)}`, 'error') - throw error // Re-throw if caller needs to handle - } - ``` - -4. **Using `any` Type:** - ```typescript - // ❌ BAD - function process(data: any) { ... } - - // βœ… GOOD - function process(data: DashboardData) { ... } - // OR if truly dynamic: - function process(data: unknown) { - if (isDashboardData(data)) { ... } - } - ``` - -5. **Duplicate Logic:** - ```typescript - // ❌ BAD: Browser creation duplicated in DesktopFlow and MobileFlow - - // βœ… GOOD: Extract to centralized utility - // src/util/BrowserFactory.ts - export async function createBrowserInstance(bot, proxy, email) { ... } - ``` - -6. **Global State:** - ```typescript - // ❌ BAD - let globalPointsEarned = 0 - - // βœ… GOOD - class MicrosoftRewardsBot { - private accountSummaries: AccountSummary[] = [] - } - ``` - -7. **Console.log for Logging:** - ```typescript - // ❌ BAD - console.log('Starting search') - - // βœ… GOOD - this.bot.log(this.bot.isMobile, 'SEARCH', 'Starting search') - ``` - ---- - -## πŸ” Code Review Checklist - -Before submitting ANY code change: - -### Functionality -- [ ] Code solves the stated problem completely -- [ ] No regressions in existing features -- [ ] Edge cases handled (null, undefined, empty arrays) -- [ ] Error handling implemented correctly - -### Code Quality -- [ ] Follows project style guide -- [ ] No code duplication (DRY principle) -- [ ] Functions are small and focused -- [ ] Variables have descriptive names -- [ ] No commented-out code (remove or explain) - -### Type Safety -- [ ] No `any` types (use proper types or `unknown`) -- [ ] Explicit return types on public functions -- [ ] Type guards for runtime validation -- [ ] Interfaces updated if data structures changed - -### Performance -- [ ] No unnecessary loops or redundant operations -- [ ] Async operations handled efficiently -- [ ] Memory leaks prevented (cleanup in `finally` blocks) -- [ ] Timeouts configured to prevent infinite hangs - -### Security -- [ ] No sensitive data logged (passwords, TOTP, tokens) -- [ ] User input validated/sanitized -- [ ] Proxy credentials handled securely -- [ ] Anti-detection measures not weakened - -### Testing -- [ ] Unit tests added/updated for new logic -- [ ] Existing tests still pass -- [ ] Manual testing performed -- [ ] Verified on both Windows and Linux (if OS-specific) - -### Documentation -- [ ] JSDoc comments on public APIs -- [ ] User-facing docs updated (`docs/`) -- [ ] Inline comments for complex logic -- [ ] README.md updated if user-facing change - ---- - -## 🎯 Best Practices Summary - -### Always Do This βœ… -1. **Use TypeScript strict mode** (already configured) -2. **Log errors with context** (use `formatDetailedError()`) -3. **Keep humanization enabled** (never disable in production) -4. **Extract reusable logic** (avoid duplication) -5. **Write in English** (code, comments, docs) -6. **Test before committing** (build + typecheck + manual test) -7. **Document public APIs** (JSDoc comments) -8. **Handle errors explicitly** (no silent catches) -9. **Use constants** (no magic numbers/strings) -10. **Verify full codebase context** (understand existing patterns) - -### Never Do This ❌ -1. **Don't commit sensitive data** (`accounts.jsonc`, `sessions/`) -2. **Don't disable humanization** (breaks anti-detection) -3. **Don't use console.log** (use `bot.log()`) -4. **Don't ignore TypeScript errors** (fix them properly) -5. **Don't duplicate code** (extract to utilities) -6. **Don't use `any` type** (use proper types) -7. **Don't create tracking docs** (summarize in chat/commit messages) -8. **Don't hardcode values** (use config or constants) -9. **Don't swallow errors** (log or propagate) -10. **Don't rush patches** (solve root cause) - ---- - -## πŸš€ Quick Reference - -### Project Commands -```bash -npm install # Install dependencies -npm run build # Compile TypeScript -npm start # Run bot (production) -npm run dev # Run bot (development) -npm run test # Run test suite -npm run typecheck # Type check only -npm run dashboard # Start dashboard server -npm run creator # Account creation wizard -``` - -### Key Files -- **Entry Point:** `src/index.ts` (main bot orchestration) -- **Config:** `src/config.jsonc` (user configuration) -- **Accounts:** `src/accounts.jsonc` (sensitive, gitignored) -- **Build Output:** `dist/` (compiled JavaScript) -- **Documentation:** `docs/` (user guides) -- **Tests:** `tests/` (unit tests) - -### Important Utilities -- **Logging:** `src/util/Logger.ts` (`log()` function) -- **Retries:** `src/util/Retry.ts` (exponential backoff) -- **Humanization:** `src/util/Humanizer.ts` (random delays) -- **Ban Detection:** `src/util/BanDetector.ts` (heuristic detection) -- **Browser Factory:** `src/util/BrowserFactory.ts` (centralized creation) -- **Error Formatting:** `src/util/Utils.ts` (`formatDetailedError()`) - -### Configuration Flags -```jsonc -{ - "humanization.enabled": true, // ALWAYS true in production - "workers.doDesktopSearch": true, // Enable desktop searches - "workers.doMobileSearch": true, // Enable mobile searches - "workers.doDailySet": true, // Enable daily activities - "execution.runOnZeroPoints": false, // Skip if no points available - "jobState.enabled": true, // Track completed tasks - "scheduling.enabled": false // Auto-schedule (cron/Task Scheduler) -} -``` - ---- - -## πŸ“ž Support & Resources - -- **Discord:** https://discord.gg/k5uHkx9mne (community support) -- **GitHub Issues:** https://github.com/LightZirconite/Microsoft-Rewards-Bot/issues -- **Documentation:** `/docs/index.md` (full guide index) -- **License:** CC BY-NC-SA 4.0 (non-commercial use) - ---- - -## πŸ”§ Key Utilities Reference - -### Core Infrastructure Utilities - -**BrowserFactory (`src/util/BrowserFactory.ts`):** -- **Purpose:** Centralized browser instance creation (eliminates Desktop/Mobile duplication) -- **Key Function:** `createBrowserInstance(bot, proxy, email)` β†’ Returns configured Playwright BrowserContext -- **Usage:** All flows use this instead of duplicating browser creation logic - -**Humanizer (`src/util/Humanizer.ts`):** -- **Purpose:** Random delays, mouse gestures, scrolling to mimic human behavior -- **Key Methods:** - - `microGestures(page)`: Random mouse moves (40% prob) and scrolls (20% prob) - - `actionPause()`: Configurable random delay between actions (default 150-450ms, max 5s) -- **Configuration:** `humanization.enabled` (MUST be true in production), `gestureMoveProb`, `gestureScrollProb`, `actionDelay` - -**BrowserUtil (`src/browser/BrowserUtil.ts`):** -- **Purpose:** Dismiss popups, overlays, cookie banners, streak dialogs, terms updates -- **Key Methods:** - - `tryDismissAllMessages(page)`: One-shot dismissal of 15+ button types - - `dismissStreakDialog()`, `dismissTermsUpdateDialog()`: Specialized handlers - - `getLatestTab()`: Get most recently opened tab - - `reloadBadPage()`: Detect and reload network/HTTP 400 errors - - `humanizePage(page)`: Combine humanization + action pause -- **Pattern:** Silent failures (popups not present = expected, don't break flow) - -**Retry (`src/util/Retry.ts`):** -- **Purpose:** Exponential backoff with jitter for transient failures -- **Configuration:** `retryPolicy` in config (maxAttempts, baseDelay, maxDelay, multiplier, jitter) -- **Key Method:** `run(asyncFn, isRetryable?)` - Only retry if predicate returns true -- **Usage:** Network calls, dashboard scraping, activity completion - -**JobState (`src/util/JobState.ts`):** -- **Purpose:** Persistent checkpoint tracking to enable resume-after-crash -- **Storage:** Per-account, per-day JSON files in `sessions/job-state/` -- **Key Methods:** - - `isDone(activity)` / `markDone(activity)`: Track individual activity completion - - `isAccountComplete(email)` / `markAccountComplete(email)`: Track full account completion - - `resetAllAccounts()`: Clear state for new day -- **Integration:** Used by `index.ts` runTasks loop, SummaryReporter, Workers - -**QueryDiversityEngine (`src/util/QueryDiversityEngine.ts`):** -- **Purpose:** Generate diverse search queries from multiple sources (avoid pattern detection) -- **Sources:** Google Trends (primary), Reddit trending, news headlines, Wikipedia random, local fallback -- **Key Methods:** - - `fetchQueries(count, sources)`: Main entry point with caching - - `getFromSource(source, count)`: Per-source fetcher - - `interleaveQueries(queries)`: Mix sources for diversity -- **Configuration:** `queryDiversity.enabled`, `sources` (array), `googleTrends.geo`/`category` - -**BanDetector (`src/util/BanDetector.ts`):** -- **Purpose:** Heuristic detection of account suspension from error messages -- **Pattern Matching:** Regex array (`BAN_PATTERNS`) matching "suspend", "lock", "restricted", "violation", etc. -- **Key Method:** `detectBanReason(error: Error | string)` β†’ Returns reason string or null -- **Integration:** Used by flows to stop execution on ban detection - -**Logger (`src/util/Logger.ts`):** -- **Purpose:** Centralized logging with console, Discord webhook, NTFY push notifications -- **Features:** - - Email redaction (if `logging.redactEmails: true`) - - Webhook buffering with rate limiting (debounce 750ms, flush interval 5s) - - Color-coded console output (chalk) - - Error formatting with stack traces -- **Key Functions:** - - `log(isMobile, title, message, level?, color?)`: Main logging function - - `logError(title, message, isMobile)`: Returns error handler for catch blocks - - `enqueueWebhookLog()`: Buffered webhook dispatch - -**Constants (`src/constants.ts`):** -- **Purpose:** Centralized constants (NO magic numbers) -- **Categories:** - - `TIMEOUTS`: SHORT (500ms), MEDIUM (1.5s), LONG (3s), DASHBOARD_WAIT (10s), LOGIN_MAX (env: 30s-10min, default 3min) - - `RETRY_LIMITS`: Dashboard reloads (2), mobile search (3), activity max iterations (15) - - `DELAYS`: Search delays, typing speed (20ms/char), quiz answer wait (2s) - - `SELECTORS`: Activity IDs, suspended account header, quiz completion markers - - `URLS`: Rewards dashboard, sign-in page, mobile API endpoint - - `DISCORD`: Webhook rate limits, embed length (1900), colors (red, green, blue) - -### Specialized Utilities - -**LoginStateDetector (`src/util/LoginStateDetector.ts`):** -- **Purpose:** Detect login state (success, 2FA required, blocked, error) -- **Usage:** Called by Login.ts to determine next action - -**MobileRetryTracker (`src/util/MobileRetryTracker.ts`):** -- **Purpose:** Track mobile search retry counts per account (mobile is flaky) -- **Usage:** MobileFlow tracks retries, aborts after threshold - -**AdaptiveThrottler (`src/util/AdaptiveThrottler.ts`):** -- **Purpose:** Dynamic delay adjustment between activities -- **Usage:** Workers.ts applies throttling between activity attempts - -**Totp (`src/util/Totp.ts`):** -- **Purpose:** Generate TOTP codes for 2FA -- **Usage:** Login.ts automatic TOTP submission - ---- - -## 🚨 Known Issues & Technical Debt - -### Files Needing Refactoring - -**Login.ts (1700+ LINES - CRITICAL):** -- **Issue:** Violates Single Responsibility Principle -- **Internal Comment (lines 17-25):** Suggests splitting into: - - `LoginFlow.ts` (main orchestration) - - `TotpHandler.ts` (2FA/TOTP logic) - - `PasskeyHandler.ts` (passkey/biometric prompts) - - `RecoveryHandler.ts` (recovery email detection) - - `SecurityDetector.ts` (ban/block detection) -- **When to Address:** Before adding new login features -- **Priority:** MEDIUM (works reliably, but maintainability suffers) - -**Search.ts (600+ LINES):** -- **Status:** Manageable but complex -- **Complexity:** Google Trends API, semantic deduplication (Jaccard similarity), stagnation detection, fallback queries -- **Consideration:** Could split into SearchOrchestrator + QueryGenerator + DeduplicationEngine if further features added -- **Priority:** LOW (currently maintainable) - -**index.ts (1700+ LINES):** -- **Status:** Acceptable for main entry point -- **Content:** Bot orchestration, clustering, account loop, job state management -- **Consideration:** Main files are allowed to be large if they're coordinators -- **Priority:** LOW (no action needed) - -### Architecture Patterns to Maintain - -**Browser Creation Pattern:** -```typescript -// βœ… CORRECT: Use centralized factory -import { createBrowserInstance } from '../util/BrowserFactory' -const browser = await createBrowserInstance(bot, account.proxy, account.email) - -// ❌ WRONG: Don't recreate Browser class instances manually -const browserInstance = new Browser(bot) -const browser = await browserInstance.createBrowser(proxy, email) -``` - -**Error Handling Pattern:** -```typescript -// βœ… CORRECT: Explicit try-catch with context -try { - await operation() -} catch (error) { - this.bot.log(this.bot.isMobile, 'OPERATION', `Failed: ${getErrorMessage(error)}`, 'error') - throw new Error(`Operation failed: ${getErrorMessage(error)}`) -} - -// ❌ WRONG: Silent catch (only for non-critical operations like popups) -try { - await operation() -} catch { /* silent */ } -``` - -**Semantic Deduplication Pattern (Search.ts):** -```typescript -// Jaccard similarity for word-level comparison (threshold 0.65) -private jaccardSimilarity(a: string, b: string): number { - const setA = new Set(a.toLowerCase().split(/\s+/)) - const setB = new Set(b.toLowerCase().split(/\s+/)) - const intersection = new Set([...setA].filter(x => setB.has(x))) - const union = new Set([...setA, ...setB]) - return union.size === 0 ? 0 : intersection.size / union.size -} - -// Combined deduplication: exact + semantic in single pass -private combinedDeduplication(queries: string[], threshold = 0.65): string[] { - const seen = new Set() - const result: string[] = [] +export class NewActivity extends ActivityHandler { + async execute(): Promise { + const activityKey = 'new-activity' - for (const query of queries) { - const normalized = query.toLowerCase().trim() - if (seen.has(normalized)) continue // Exact duplicate - - // Semantic similarity check - if (result.some(existing => this.jaccardSimilarity(existing, query) >= threshold)) { - continue - } - - seen.add(normalized) - result.push(query) + // Check if already done + if (this.bot.accountJobState.isCompleted(this.bot.currentAccountEmail!, activityKey)) { + return } - return result + // Execute activity + await this.performActivity() + + // Mark complete + this.bot.accountJobState.markCompleted(this.bot.currentAccountEmail!, activityKey) + } + + private async performActivity(): Promise { + // Implementation + } } ``` ---- +2. **Register in Workers.ts**: +```typescript +if (this.bot.config.workers.doNewActivity) { + await this.runActivity('NewActivity', async () => { + const activity = new NewActivity(this.bot) + await activity.execute() + }) +} +``` -## πŸ“š Additional Context +3. **Add config option**: +```typescript +// interface/Config.ts +export interface ConfigWorkers { + doNewActivity: boolean + // ... other activities +} +``` -### Version Management -**Version Source:** Read dynamically from `package.json` (field: `version`) -- **Current Version (as of this writing):** 2.56.5 -- **Code Location:** `src/index.ts` line 235-247 (`getVersion()` method) -- **Implementation:** - ```typescript - private getVersion(): string { - const DEFAULT_VERSION = '2.56.0' - try { - const pkgPath = path.join(__dirname, '../package.json') - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) - return pkg.version || DEFAULT_VERSION - } catch { - // Ignore: Fall back to default version if package.json is unavailable - } - return DEFAULT_VERSION +### Adding a Dashboard API Endpoint + +```typescript +// src/dashboard/routes.ts +apiRouter.get('/api/new-endpoint', async (req: Request, res: Response) => { + try { + // Validate input + const param = req.query.param as string + if (!param) { + return sendError(res, 400, 'Missing param') + } + + // Process + const result = await processData(param) + + // Respond + res.json({ success: true, data: result }) + } catch (error) { + sendError(res, 500, getErr(error)) } - ``` -- **NEVER hardcode version numbers in documentation** - always reference package.json - -### Docker & Scheduling Context - -**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`) - - Cron schedule registration (env: `CRON_SCHEDULE` required) - - Playwright browser preinstallation (`PLAYWRIGHT_BROWSERS_PATH=0`) -- **Usage:** Docker Compose sets `CRON_SCHEDULE`, container runs cron in foreground - -**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 - - Called by cron and initial run -- **Pattern:** Used in both Docker and native cron setups - -### Dashboard Architecture - -**BotController (`src/dashboard/BotController.ts`):** -- **Purpose:** Bot lifecycle management for web dashboard -- **Key Methods:** - - `start()`: Initialize bot instance, run asynchronously, return immediately - - `stop()`: Cleanup bot instance (note: current task completes first) - - `restart()`: stop() + wait 2s + start() - - `getStatus()`: Return running state, PID, uptime, start time -- **Race Condition Fix:** `isStarting` flag prevents multiple simultaneous start() calls -- **State Management:** Updates `dashboardState` singleton for WebSocket broadcast - -**Dashboard Features:** -- **Real-time updates:** WebSocket connection for live progress -- **Session management:** Load/save account sessions (cookies, tokens) -- **Manual controls:** Start/stop bot, reset job state, view account status -- **Log streaming:** Buffered webhook logs displayed in UI - -### Activity System Details - -**Activity Types (9 implementations in `src/functions/activities/`):** -1. **ABC.ts:** "ABC" promotional activity (multi-choice selection) -2. **DailyCheckIn.ts:** Mobile daily check-in (mobile-only API) -3. **Poll.ts:** Single-choice poll (click one option) -4. **Quiz.ts:** Multi-question quiz (8 questions, correctAnswer detection) -5. **ReadToEarn.ts:** Mobile article reading (mobile-only API) -6. **Search.ts:** Bing search automation (600+ lines, Google Trends integration) -7. **SearchOnBing.ts:** "Search on Bing" promotional link (click + wait 5s) -8. **ThisOrThat.ts:** "This or That" game (10 rounds, binary choice) -9. **UrlReward.ts:** Generic URL reward (navigate + wait for points) - -**Activity Dispatcher (`Activities.ts`):** -- **Pattern:** Type classification β†’ handler delegation -- **Classification Logic:** Detect activity type from promotionType, title, attributes -- **Fallback:** Unknown types logged as warnings, not errors - -### Search Engine Diversity - -**QueryDiversityEngine Sources:** -1. **google-trends (PRIMARY):** - - API: `https://trends.google.com/trends/api/dailytrends` - - Configuration: `geo` (country code), `category` (optional) - - Parsing: JSON response with trending queries - -2. **reddit:** - - API: `https://www.reddit.com/r/all/hot.json` - - Extraction: Post titles from hot posts - -3. **news:** - - API: `https://news.google.com/rss` (RSS feed) - - Extraction: Headlines from RSS items - -4. **wikipedia:** - - API: `https://en.wikipedia.org/w/api.php?action=query&list=random` - - Extraction: Random article titles - -5. **local-fallback:** - - Source: Embedded `queries.json` file - - Usage: When all external sources fail - - Content: Pre-generated generic queries - -**Query Interleaving Strategy:** -- Round-robin mixing of sources -- Semantic deduplication (Jaccard similarity β‰₯ 0.65) -- Exact duplicate removal -- Minimum length filter (4 characters) - ---- - -## 🎨 Advanced Features & Hidden Gems - -### Account Creation System (`src/account-creation/`) - -**AccountCreator.ts (2671 LINES - MASSIVE FEATURE):** -- **Purpose:** Fully automated Microsoft account registration with CAPTCHA support -- **Features:** Realistic data generation, CAPTCHA detection + human-solving wait, email suggestion handling, domain reservation detection, post-creation 2FA setup, recovery email config, passkey refusal, marketing opt-out, referral link integration, session persistence -- **CLI:** `-y` (auto-accept), `http://...` (referral URL), `email@...` (recovery email) -- **Usage:** `npm run creator` -- **Key Methods:** `generateAndFillEmail()`, `handleEmailTaken()`, `fillBirthdate()`, `fillNames()`, `waitForCaptcha()`, `setup2FA()`, `setupRecoveryEmail()`, `verifyAccountActive()` -- **Patterns:** Smart verification (Microsoft domain separation), retry operations, dropdown handling, page stability checks, error recovery flows - -**DataGenerator.ts:** -- **Methods:** `generateEmail()` (8 realistic patterns), `generatePassword()` (14-18 chars), `generateBirthdate()` (age 20-45), `generateNames()` (extracts from email) -- **Pattern:** Uses nameDatabase.ts with 100+ first/last names - -### Auto-Update System (`scripts/installer/update.mjs`) - -**update.mjs (600+ LINES - CRITICAL FEATURE):** -- **Purpose:** Git-free update system using GitHub ZIP downloads (NO merge conflicts!) -- **Location:** `scripts/installer/update.mjs` -- **Features:** Version comparison (cache-busting), GitHub API ZIP download, selective file preservation, automatic rollback on build failure, integrity checks, Docker vs Host detection, dependency installation, TypeScript rebuild verification, update marker creation -- **Protected Files:** `src/config.jsonc`, `src/accounts.jsonc`, `sessions/`, `.playwright-chromium-installed` -- **Workflow:** Check version β†’ Create backups β†’ Download ZIP β†’ Extract β†’ Selective copy β†’ Restore protected β†’ npm ci β†’ npm install β†’ npm build β†’ Verify integrity β†’ Create marker β†’ Clean temp -- **Docker Behavior:** Exits cleanly to let orchestrator restart -- **Host Behavior:** Signals restart needed (bot detects `.update-happened` marker) - -### Startup Validation System (`src/util/StartupValidator.ts`) - -**StartupValidator.ts (500+ LINES - PRODUCTION SAFETY):** -- **Purpose:** Comprehensive configuration validation before bot starts (prevents runtime crashes!) -- **11 Categories:** Accounts, Config, Environment, File System, Browser, Network, Workers, Execution, Search, Humanization, Security -- **Error Types:** Blocking (prevent startup), Non-Blocking (warn), Warnings (best practices) -- **Validations:** Email format, password presence, TOTP Base32 format, Node.js β‰₯18, webhook URLs (http/https), action delay min≀max, gesture probabilities (0-1), allowed windows format, clusters 1-10, retry counts, etc. -- **Display:** Color-coded (red errors, yellow warnings), fix suggestions, documentation links, 3s pause if errors - -### Conclusion Webhook System (`src/util/ConclusionWebhook.ts`) - -**Features:** Discord embed format, dual webhook support (deduplicated URLs), retry logic (3 attempts, exponential backoff: 1s, 2s, 4s), optional NTFY integration, avatar + username customization - -### Error Reporting System (Vercel Serverless) (`api/report-error.ts`, `src/util/notifications/ErrorReportingWebhook.ts`) - -**MAJOR REDESIGN (2025-01-02):** Complete rewrite from Discord webhooks β†’ Vercel Serverless Functions - -**OLD SYSTEM (Pre-2025) - DEPRECATED:** -- ❌ Hardcoded Discord webhooks (4 redundancy URLs in base64) -- ❌ AES-256-GCM obfuscation with `ERROR_WEBHOOK_KEY` -- ❌ Webhook rotation logic (`disabled-webhooks.json` tracking) -- ❌ Users could disable webhooks (config control) -- ❌ ~600 lines of complex code -- ❌ Disabled since 2024-12-26 due to vulnerabilities - -**NEW SYSTEM (2025+) - ACTIVE:** -- βœ… Vercel Serverless Function: `api/report-error.ts` -- βœ… Webhook URL in Vercel environment variables (NEVER in code) -- βœ… Server-side rate limiting (10 req/min/IP, bypass with `X-Rate-Limit-Secret`) -- βœ… ~300 lines of clean code (50% reduction) -- βœ… Free Vercel tier (100,000 requests/day) -- βœ… Maintainer-controlled (users can opt-out but not break system) - -**Architecture:** -``` -Bot β†’ POST /api/report-error β†’ Vercel Function β†’ Discord Webhook - (sanitized payload) (env vars) (maintainer's server) +}) ``` -**Key Features:** -- **Sanitization:** Redacts emails, paths, IPs, tokens (SANITIZE_PATTERNS) -- **Filtering:** Skips user config errors, expected errors (shouldReportError) -- **Payload:** Error message, stack trace (15 lines), version, platform, botMode -- **Config:** `errorReporting.enabled`, `apiUrl`, `secret` (optional bypass) +### Debugging -**Files:** -- `api/report-error.ts` - Vercel serverless function (TypeScript) -- `vercel.json` - Vercel deployment config -- `api/README.md` - Setup instructions for maintainers -- `docs/error-reporting-vercel.md` - Full documentation -- `src/interface/Config.ts` - `ConfigErrorReporting` interface +**Enable verbose logging**: +```bash +# Full debug output +DEBUG_REWARDS_VERBOSE=1 npm start -**Setup (Maintainers):** -1. Add `DISCORD_ERROR_WEBHOOK_URL` to Vercel env vars -2. Optional: Add `RATE_LIMIT_SECRET` for trusted clients -3. Deploy: `git push` or `vercel --prod` -4. Test: `curl -X POST https://rewards-bot-eight.vercel.app/api/report-error` +# Specific module debug +DEBUG=playwright:* npm start +``` -**Migration Guide:** -- Old `errorReporting.webhooks[]` config field DEPRECATED (still supported as fallback) -- Old `sessions/disabled-webhooks.json` file NO LONGER USED -- Bot automatically uses new API (no user action required) +**Common debug points**: +- Browser: `this.browser.func.makeDebugScreenshot(page, 'issue-name')` +- State: `console.log(this.accountJobState.getState(email))` +- Network: `await this.axios.request(config)` (auto-logged) --- -**Last Updated:** 2025-01-02 +## Testing Checklist + +Before committing changes: +- [ ] `npm run build` succeeds +- [ ] `npm run lint` passes (or `npm run lint:fix`) +- [ ] Test with `npm start` (single account) +- [ ] Test dashboard with `npm run dashboard` +- [ ] Check JobState resume works (stop mid-run, restart) +- [ ] Verify humanization delays are active +- [ ] Test error handling (disconnect network mid-run) +- [ ] Check logs are informative and properly formatted --- -*This file provides comprehensive guidance for GitHub Copilot to maintain code quality, architecture consistency, and long-term maintainability of the Microsoft Rewards Bot project.* +## File Naming Conventions + +- **Interfaces**: PascalCase (`Account.ts`, `Config.ts`) +- **Classes**: PascalCase (`BrowserFunc.ts`, `StatsManager.ts`) +- **Utilities**: PascalCase (`Utils.ts`, `Logger.ts`) +- **Constants**: SCREAMING_SNAKE_CASE in `constants.ts` +- **Config files**: kebab-case + .jsonc extension + +--- + +## Prohibited Patterns + +❌ **DO NOT**: +- Hardcode credentials or sensitive data +- Use `any` type (use `unknown` + type guards) +- Directly manipulate `dist/` (auto-generated from `src/`) +- Create `.json` configs (use `.jsonc` with comments) +- Ignore TypeScript errors (fix properly) +- Use `console.log()` directly (use `log()` function) +- Block main thread with `while(true)` loops +- Store passwords in plaintext outside `accounts.jsonc` + +βœ… **DO**: +- Use strict TypeScript checks +- Add JSDoc comments for public APIs +- Handle all promise rejections +- Use `try/finally` for cleanup +- Validate external input +- Use environment variables for secrets when possible +- Test on Windows + Linux (cross-platform compatible) + +--- + +## Key Dependencies + +| Package | Purpose | Notes | +|---------|---------|-------| +| `rebrowser-playwright` | Browser automation | Patched for detection evasion | +| `fingerprint-generator` | Browser fingerprinting | Generates realistic fingerprints | +| `express` | Dashboard server | RESTful API + WebSockets | +| `axios` | HTTP client | Used for Rewards API calls | +| `cheerio` | HTML parsing | Extracting activity data | +| `chalk` | Terminal colors | Pretty console output | + +--- + +## Update Process + +1. **GitHub releases**: Bot auto-updates from GitHub +2. **Config updates**: Use `autoUpdateConfig: false` to preserve customizations +3. **Accounts updates**: Use `autoUpdateAccounts: false` to preserve credentials +4. **Smart merge**: Compares local vs remote, only updates if unchanged + +**See**: `scripts/installer/update.mjs` for implementation + +--- + +## Performance Guidelines + +- **Memory**: Target < 500MB per account (monitor with `MemoryMonitor.ts`) +- **Execution time**: ~5-10 minutes per account (desktop + mobile) +- **Concurrent accounts**: Default 1 cluster, max 4 recommended +- **Rate limiting**: Respect Bing search delays (3-5 min default) + +--- + +## Support & Community + +- **GitHub Issues**: Bug reports and feature requests +- **Discord**: Real-time community support +- **Docs**: `docs/` folder contains detailed guides + +--- + +**Last Updated**: January 2026 +**Maintainer**: LightZirconite diff --git a/README.md b/README.md index 6d7b886..4644a81 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,18 @@ git clone https://github.com/LightZirconite/Microsoft-Rewards-Bot.git cd Microsoft-Rewards-Bot -# 2. Setup accounts -cp src/accounts.example.jsonc src/accounts.jsonc +# 2. Run the bot (auto-creates config files on first run) +npm start + +# 3. Configure your accounts # Edit src/accounts.jsonc with your Microsoft account(s) -# 3. Run +# 4. Run again npm start ``` +**Note:** Configuration files (`config.jsonc` and `accounts.jsonc`) are automatically created from `.example.jsonc` templates on first run. + ## Features | Feature | Description | diff --git a/scripts/installer/README.md b/scripts/installer/README.md deleted file mode 100644 index 7a0059e..0000000 --- a/scripts/installer/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# Update System - -## Overview - -The bot uses a **simplified GitHub API-based update system** that: -- βœ… Downloads latest code as ZIP archive -- βœ… No Git required -- βœ… No merge conflicts -- βœ… Preserves user files automatically -- βœ… Automatic dependency installation -- βœ… TypeScript rebuild - -## How It Works - -1. **Automatic Updates**: If enabled in `config.jsonc`, the bot checks for updates on every startup -2. **Download**: Latest code is downloaded as ZIP from GitHub -3. **Protection**: User files (accounts, config, sessions) are backed up -4. **Update**: Code files are replaced selectively -5. **Restore**: Protected files are restored -6. **Install**: Dependencies are installed (`npm ci`) -7. **Build**: TypeScript is compiled -8. **Restart**: Bot restarts automatically with new version - -## Configuration - -In `src/config.jsonc`: - -```jsonc -{ - "update": { - "enabled": true, // Enable/disable updates - "autoUpdateAccounts": false, // Protect accounts files (recommended: false) - "autoUpdateConfig": false // Protect config.jsonc (recommended: false) - } -} -``` - -## Protected Files - -These files are **always protected** (never overwritten): -- `sessions/` - Browser session data -- `.playwright-chromium-installed` - Browser installation marker - -These files are **conditionally protected** (based on config): -- `src/accounts.jsonc` - Protected unless `autoUpdateAccounts: true` -- `src/accounts.json` - Protected unless `autoUpdateAccounts: true` -- `src/config.jsonc` - Protected unless `autoUpdateConfig: true` - -## Manual Update - -Run the update manually: - -```bash -node scripts/installer/update.mjs -``` - -## Update Detection - -The bot uses marker files to prevent restart loops: - -- `.update-happened` - Created when files are actually updated -- `.update-restart-count` - Tracks restart attempts (max 3) - -If no updates are available, **no marker is created** and the bot won't restart. - -## Troubleshooting - -### Updates disabled -``` -⚠️ Updates are disabled in config.jsonc -``` -β†’ Set `update.enabled: true` in `src/config.jsonc` - -### Download failed -``` -❌ Download failed: [error] -``` -β†’ Check your internet connection -β†’ Verify GitHub is accessible - -### Extraction failed -``` -❌ Extraction failed: [error] -``` -β†’ Ensure you have one of: `unzip`, `tar`, or PowerShell (Windows) - -### Build failed -``` -⚠️ Update completed with build warnings -``` -β†’ Check TypeScript errors above -β†’ May still work, but review errors - -## Architecture - -### File Structure -``` -scripts/installer/ - β”œβ”€β”€ update.mjs # Main update script (auto-updater) - β”œβ”€β”€ setup.mjs # Initial setup wizard - └── README.md # This file -``` - -### Update Flow -``` -Start - ↓ -Check config (enabled?) - ↓ -Read user preferences (autoUpdate flags) - ↓ -Backup protected files - ↓ -Download ZIP from GitHub - ↓ -Extract archive - ↓ -Copy files selectively (skip protected) - ↓ -Restore protected files - ↓ -Cleanup temporary files - ↓ -Create marker (.update-happened) if files changed - ↓ -Install dependencies (npm ci) - ↓ -Build TypeScript - ↓ -Exit (bot auto-restarts if marker exists) -``` - -## Previous System - -The old update system (799 lines) supported two methods: -- Git method (required Git, had merge conflicts) -- GitHub API method - -**New system**: Only GitHub API method (simpler, more reliable) - -## Anti-Loop Protection - -The bot has built-in protection against infinite restart loops: - -1. **Marker detection**: Bot only restarts if `.update-happened` exists -2. **Restart counter**: Max 3 restart attempts (`.update-restart-count`) -3. **Counter cleanup**: Removed after successful run without updates -4. **No-update detection**: Marker NOT created if already up to date - -This ensures the bot never gets stuck in an infinite update loop. - -## Dependencies - -No external dependencies required! The update system uses only Node.js built-in modules: -- `node:child_process` - Run shell commands -- `node:fs` - File system operations -- `node:https` - Download files -- `node:path` - Path manipulation - -## Exit Codes - -- `0` - Success (updated or already up to date) -- `1` - Error (download failed, extraction failed, etc.) - -## NPM Scripts - -- `npm run start` - Start bot (runs update check first if enabled) -- `npm run dev` - Start in dev mode (skips update check) -- `npm run build` - Build TypeScript manually - -## Version Info - -- Current version: **v2** (GitHub API only) -- Previous version: v1 (Dual Git/GitHub API) -- Lines of code: **468** (down from 799) -- Complexity: **Simple** (down from Complex) diff --git a/scripts/installer/update.mjs b/scripts/installer/update.mjs index 7ff479b..fb0c508 100644 --- a/scripts/installer/update.mjs +++ b/scripts/installer/update.mjs @@ -264,85 +264,208 @@ function getUpdateMode(configData) { // ============================================================================= /** - * Check if update is available by comparing versions - * Uses GitHub API directly (no CDN cache issues, always fresh data) - * Rate limit: 60 requests/hour (sufficient for bot updates) + * Download a file from GitHub raw URL */ -async function checkVersion() { - try { - // Read local version - const localPkgPath = join(process.cwd(), 'package.json') - if (!existsSync(localPkgPath)) { - console.log('⚠️ Could not find local package.json') - return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' } - } +async function downloadFromGitHub(url, dest) { + console.log(`πŸ“₯ Downloading: ${url}`) - const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8')) - const localVersion = localPkg.version + return new Promise((resolve, reject) => { + const file = createWriteStream(dest) - // Fetch remote version from GitHub API (no cache) - const repoOwner = 'LightZirconite' - const repoName = 'Microsoft-Rewards-Bot' - const branch = 'main' - - console.log('πŸ” Checking for updates...') - console.log(` Local: ${localVersion}`) - - // Use GitHub API directly - no CDN cache, always fresh - const apiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/package.json?ref=${branch}` - - return new Promise((resolve) => { - const options = { - headers: { - 'User-Agent': 'Microsoft-Rewards-Bot-Updater', - 'Accept': 'application/vnd.github.v3.raw', // Returns raw file content - 'Cache-Control': 'no-cache' - } + httpsGet(url, { + headers: { + 'User-Agent': 'Microsoft-Rewards-Bot-Updater', + 'Cache-Control': 'no-cache' + } + }, (response) => { + // Handle redirects + if (response.statusCode === 302 || response.statusCode === 301) { + file.close() + rmSync(dest, { force: true }) + downloadFromGitHub(response.headers.location, dest).then(resolve).catch(reject) + return } - const request = httpsGet(apiUrl, options, (res) => { - if (res.statusCode !== 200) { - console.log(` ⚠️ GitHub API returned HTTP ${res.statusCode}`) - if (res.statusCode === 403) { - console.log(' ℹ️ Rate limit may be exceeded (60/hour). Try again later.') - } - resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) - return - } + if (response.statusCode !== 200) { + file.close() + rmSync(dest, { force: true }) + reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)) + return + } - let data = '' - res.on('data', chunk => data += chunk) - res.on('end', () => { - try { - const remotePkg = JSON.parse(data) - const remoteVersion = remotePkg.version - console.log(` Remote: ${remoteVersion}`) - - // Any difference triggers update (upgrade or downgrade) - const updateAvailable = localVersion !== remoteVersion - resolve({ updateAvailable, localVersion, remoteVersion }) - } catch (err) { - console.log(` ⚠️ Could not parse remote package.json: ${err.message}`) - resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) - } - }) - }) - - request.on('error', (err) => { - console.log(` ⚠️ Network error: ${err.message}`) - resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) - }) - - request.setTimeout(10000, () => { - request.destroy() - console.log(' ⚠️ Request timeout (10s)') - resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + response.pipe(file) + file.on('finish', () => { + file.close() + resolve() }) + }).on('error', (err) => { + file.close() + rmSync(dest, { force: true }) + reject(err) }) - } catch (err) { - console.log(`⚠️ Version check failed: ${err.message}`) + }) +} + +/** + * Smart update for config/accounts example files + * Only updates if GitHub version has changed AND local user file matches old example + */ +async function smartUpdateExampleFiles(configData) { + const files = [] + + // Check which files to update based on config + if (configData?.update?.autoUpdateConfig === true) { + files.push({ + example: 'src/config.example.jsonc', + target: 'src/config.jsonc', + name: 'Configuration', + githubUrl: 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/src/config.example.jsonc' + }) + } + + if (configData?.update?.autoUpdateAccounts === true) { + files.push({ + example: 'src/accounts.example.jsonc', + target: 'src/accounts.jsonc', + name: 'Accounts', + githubUrl: 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/src/accounts.example.jsonc' + }) + } + + if (files.length === 0) { + return // Nothing to update + } + + console.log('\nπŸ”§ Checking for example file updates...') + + for (const file of files) { + try { + const examplePath = join(process.cwd(), file.example) + const targetPath = join(process.cwd(), file.target) + const tempPath = join(process.cwd(), `.update-${file.example.split('/').pop()}`) + + // Download latest version from GitHub + await downloadFromGitHub(file.githubUrl, tempPath) + + // Read all versions + const githubContent = readFileSync(tempPath, 'utf8') + const localExampleContent = existsSync(examplePath) ? readFileSync(examplePath, 'utf8') : '' + const userContent = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : '' + + // Check if GitHub version is different from local example + if (githubContent === localExampleContent) { + console.log(`βœ“ ${file.name}: No changes detected`) + rmSync(tempPath, { force: true }) + continue + } + + // GitHub version is different - check if user has modified their file + if (userContent === localExampleContent) { + // User hasn't modified their file - safe to update + console.log(`πŸ“ ${file.name}: Updating to latest version...`) + + // Update example file + writeFileSync(examplePath, githubContent) + + // Update user file (since they haven't customized it) + writeFileSync(targetPath, githubContent) + + console.log(`βœ… ${file.name}: Updated successfully`) + } else { + // User has customized their file - DO NOT overwrite + console.log(`⚠️ ${file.name}: User has custom changes, skipping auto-update`) + console.log(` β†’ Update available in: ${file.example}`) + console.log(` β†’ To disable this check: set "update.autoUpdate${file.name === 'Configuration' ? 'Config' : 'Accounts'}" to false`) + + // Still update the example file for reference + writeFileSync(examplePath, githubContent) + } + + // Clean up temp file + rmSync(tempPath, { force: true }) + + } catch (error) { + console.error(`❌ Failed to update ${file.name}: ${error.message}`) + // Continue with other files + } + } + + console.log('') +} +try { + // Read local version + const localPkgPath = join(process.cwd(), 'package.json') + if (!existsSync(localPkgPath)) { + console.log('⚠️ Could not find local package.json') return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' } } + + const localPkg = JSON.parse(readFileSync(localPkgPath, 'utf8')) + const localVersion = localPkg.version + + // Fetch remote version from GitHub API (no cache) + const repoOwner = 'LightZirconite' + const repoName = 'Microsoft-Rewards-Bot' + const branch = 'main' + + console.log('πŸ” Checking for updates...') + console.log(` Local: ${localVersion}`) + + // Use GitHub API directly - no CDN cache, always fresh + const apiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/package.json?ref=${branch}` + + return new Promise((resolve) => { + const options = { + headers: { + 'User-Agent': 'Microsoft-Rewards-Bot-Updater', + 'Accept': 'application/vnd.github.v3.raw', // Returns raw file content + 'Cache-Control': 'no-cache' + } + } + + const request = httpsGet(apiUrl, options, (res) => { + if (res.statusCode !== 200) { + console.log(` ⚠️ GitHub API returned HTTP ${res.statusCode}`) + if (res.statusCode === 403) { + console.log(' ℹ️ Rate limit may be exceeded (60/hour). Try again later.') + } + resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + return + } + + let data = '' + res.on('data', chunk => data += chunk) + res.on('end', () => { + try { + const remotePkg = JSON.parse(data) + const remoteVersion = remotePkg.version + console.log(` Remote: ${remoteVersion}`) + + // Any difference triggers update (upgrade or downgrade) + const updateAvailable = localVersion !== remoteVersion + resolve({ updateAvailable, localVersion, remoteVersion }) + } catch (err) { + console.log(` ⚠️ Could not parse remote package.json: ${err.message}`) + resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + } + }) + }) + + request.on('error', (err) => { + console.log(` ⚠️ Network error: ${err.message}`) + resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + }) + + request.setTimeout(10000, () => { + request.destroy() + console.log(' ⚠️ Request timeout (10s)') + resolve({ updateAvailable: false, localVersion, remoteVersion: 'unknown' }) + }) + }) +} catch (err) { + console.log(`⚠️ Version check failed: ${err.message}`) + return { updateAvailable: false, localVersion: 'unknown', remoteVersion: 'unknown' } +} } /** @@ -615,6 +738,9 @@ async function performUpdate() { process.stdout.write(' βœ“\n') + // Step 10.5: Smart update example files (config/accounts) if enabled + await smartUpdateExampleFiles(configData) + // Step 11: Verify integrity (check if critical files exist AND were recently updated) process.stdout.write('πŸ” Verifying integrity...') const criticalPaths = [ diff --git a/src/config.example.jsonc b/src/config.example.jsonc index 55fc9e5..334cd82 100644 --- a/src/config.example.jsonc +++ b/src/config.example.jsonc @@ -147,8 +147,8 @@ "enabled": true, // Enable automatic update checks "method": "github-api", // Update method (github-api = recommended) "dockerMode": "auto", // Docker detection ("auto", "force-docker", "force-host") - "autoUpdateConfig": false, // Automatically update config.jsonc (NOT RECOMMENDED - use config.example.jsonc) - "autoUpdateAccounts": false // Automatically update accounts.jsonc (NEVER enable - prevents data loss) + "autoUpdateConfig": true, // Automatically update config.jsonc + "autoUpdateAccounts": false // Automatically update accounts.jsonc }, // === ERROR REPORTING === // Help improve the project by automatically reporting errors diff --git a/src/dashboard/BotController.ts b/src/dashboard/BotController.ts index d1101ba..4309e12 100644 --- a/src/dashboard/BotController.ts +++ b/src/dashboard/BotController.ts @@ -7,6 +7,8 @@ export class BotController { private botInstance: MicrosoftRewardsBot | null = null private startTime?: Date private isStarting: boolean = false // Race condition protection + private stopRequested: boolean = false // Stop signal flag + private botProcess: Promise | null = null // Track bot execution constructor() { process.on('exit', () => this.stop()) @@ -46,21 +48,34 @@ export class BotController { dashboardState.setBotInstance(this.botInstance) // Run bot asynchronously - don't block the API response - void (async () => { + this.botProcess = (async () => { try { this.log('βœ“ Bot initialized, starting execution...', 'log') await this.botInstance!.initialize() + + // Check for stop signal before running + if (this.stopRequested) { + this.log('⚠ Stop requested before execution - aborting', 'warn') + return + } + await this.botInstance!.run() this.log('βœ“ Bot completed successfully', 'log') } catch (error) { - this.log(`Bot error: ${getErrorMessage(error)}`, 'error') + // Check if error was due to stop signal + if (this.stopRequested) { + this.log('⚠ Bot stopped by user request', 'warn') + } else { + this.log(`Bot error: ${getErrorMessage(error)}`, 'error') + } } finally { this.cleanup() } })() + // Don't await - return immediately to unblock API return { success: true, pid: process.pid } } catch (error) { @@ -73,20 +88,30 @@ export class BotController { } } - public stop(): { success: boolean; error?: string } { + public async stop(): Promise<{ success: boolean; error?: string }> { if (!this.botInstance) { return { success: false, error: 'Bot is not running' } } try { this.log('πŸ›‘ Stopping bot...', 'warn') - this.log('⚠ Note: Bot will complete current task before stopping', 'warn') + this.log('⚠ Bot will complete current task before stopping', 'warn') + + // Set stop flag + this.stopRequested = true + + // Wait for bot process to finish (with timeout) + if (this.botProcess) { + const timeout = new Promise((resolve) => setTimeout(resolve, 10000)) // 10s timeout + await Promise.race([this.botProcess, timeout]) + } this.cleanup() + this.log('βœ“ Bot stopped successfully', 'log') return { success: true } } catch (error) { - const errorMsg = getErrorMessage(error) + const errorMsg = await getErrorMessage(error) this.log(`Error stopping bot: ${errorMsg}`, 'error') this.cleanup() return { success: false, error: errorMsg } @@ -96,7 +121,7 @@ export class BotController { public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> { this.log('πŸ”„ Restarting bot...', 'log') - const stopResult = this.stop() + const stopResult = await this.stop() if (!stopResult.success && stopResult.error !== 'Bot is not running') { return { success: false, error: `Failed to stop: ${stopResult.error}` } } @@ -143,14 +168,21 @@ export class BotController { dashboardState.updateAccount(email, { status: 'running', errors: [] }) // Run bot asynchronously with single account - void (async () => { + this.botProcess = (async () => { try { this.log(`βœ“ Bot initialized for ${email}, starting execution...`, 'log') await this.botInstance!.initialize() - // Override accounts to run only this one - ; (this.botInstance as any).accounts = [targetAccount] + // Check for stop signal + if (this.stopRequested) { + this.log(`⚠ Stop requested for ${email} - aborting`, 'warn') + dashboardState.updateAccount(email, { status: 'idle' }) + return + } + + // Override accounts to run only this one + ; (this.botInstance as any).accounts = [targetAccount] await this.botInstance!.run() @@ -158,11 +190,16 @@ export class BotController { dashboardState.updateAccount(email, { status: 'completed' }) } catch (error) { const errMsg = error instanceof Error ? error.message : String(error) - this.log(`Bot error for ${email}: ${errMsg}`, 'error') - dashboardState.updateAccount(email, { - status: 'error', - errors: [errMsg] - }) + if (this.stopRequested) { + this.log(`⚠ Bot stopped for ${email} by user request`, 'warn') + dashboardState.updateAccount(email, { status: 'idle' }) + } else { + this.log(`Bot error for ${email}: ${errMsg}`, 'error') + dashboardState.updateAccount(email, { + status: 'error', + errors: [errMsg] + }) + } } finally { this.cleanup() } @@ -202,6 +239,8 @@ export class BotController { private cleanup(): void { this.botInstance = null this.startTime = undefined + this.stopRequested = false + this.botProcess = null dashboardState.setRunning(false) dashboardState.setBotInstance(undefined) } diff --git a/src/dashboard/StatsManager.ts b/src/dashboard/StatsManager.ts index 98b4f6c..a6eb2bf 100644 --- a/src/dashboard/StatsManager.ts +++ b/src/dashboard/StatsManager.ts @@ -256,6 +256,68 @@ export class StatsManager { console.error('[STATS] Failed to prune old stats:', error) } } + + /** + * Get historical stats for charts (last N days) + */ + getHistoricalStats(days: number = 30): Record { + const result: Record = {} + const today = new Date() + + for (let i = 0; i < days; i++) { + const date = new Date(today) + date.setDate(date.getDate() - i) + const dateStr = date.toISOString().slice(0, 10) + + const stats = this.loadDailyStats(dateStr) + result[dateStr] = stats?.totalPoints || 0 + } + + return result + } + + /** + * Get activity breakdown for last N days + */ + getActivityBreakdown(days: number = 7): Record { + const breakdown: Record = { + 'Desktop Search': 0, + 'Mobile Search': 0, + 'Daily Set': 0, + 'Quizzes': 0, + 'Punch Cards': 0, + 'Other': 0 + } + + const today = new Date() + for (let i = 0; i < days; i++) { + const date = new Date(today) + date.setDate(date.getDate() - i) + const dateStr = date.toISOString().slice(0, 10) + + const accountStats = this.getAccountStatsForDate(dateStr) + + for (const account of accountStats) { + if (!account.desktopSearches) account.desktopSearches = 0 + if (!account.mobileSearches) account.mobileSearches = 0 + if (!account.activitiesCompleted) account.activitiesCompleted = 0 + + breakdown['Desktop Search']! += account.desktopSearches + breakdown['Mobile Search']! += account.mobileSearches + breakdown['Daily Set']! += Math.min(1, account.activitiesCompleted) + breakdown['Other']! += Math.max(0, account.activitiesCompleted - 3) + } + } + + return breakdown + } + + /** + * Get global stats + */ + getGlobalStats(): GlobalStats { + return this.loadGlobalStats() + } } // Singleton instance diff --git a/src/dashboard/routes.ts b/src/dashboard/routes.ts index faed274..08645fe 100644 --- a/src/dashboard/routes.ts +++ b/src/dashboard/routes.ts @@ -173,9 +173,9 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise => }) // POST /api/stop - Stop bot -apiRouter.post('/stop', (_req: Request, res: Response): void => { +apiRouter.post('/stop', async (_req: Request, res: Response): Promise => { try { - const result = botController.stop() + const result = await botController.stop() if (result.success) { sendSuccess(res, { message: 'Bot stopped successfully' }) @@ -484,6 +484,38 @@ apiRouter.get('/account-stats/:email', (req: Request, res: Response) => { } }) +// GET /api/stats/historical - Get historical point data +apiRouter.get('/stats/historical', (req: Request, res: Response) => { + try { + const days = parseInt(req.query.days as string) || 30 + const historical = statsManager.getHistoricalStats(days) + res.json(historical) + } catch (error) { + res.status(500).json({ error: getErr(error) }) + } +}) + +// GET /api/stats/activity-breakdown - Get activity breakdown +apiRouter.get('/stats/activity-breakdown', (req: Request, res: Response) => { + try { + const days = parseInt(req.query.days as string) || 7 + const breakdown = statsManager.getActivityBreakdown(days) + res.json(breakdown) + } catch (error) { + res.status(500).json({ error: getErr(error) }) + } +}) + +// GET /api/stats/global - Get global statistics +apiRouter.get('/stats/global', (_req: Request, res: Response) => { + try { + const global = statsManager.getGlobalStats() + res.json(global) + } catch (error) { + res.status(500).json({ error: getErr(error) }) + } +}) + // Helper to mask sensitive URLs function maskUrl(url: string): string { try { diff --git a/src/index.ts b/src/index.ts index 1fe0a46..e0be825 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ import { InternalScheduler } from './scheduler/InternalScheduler' import { DISCORD, TIMEOUTS } from './constants' import { Account } from './interface/Account' +import { FileBootstrap } from './util/core/FileBootstrap' // Main bot class @@ -1137,6 +1138,21 @@ async function main(): Promise { const bootstrap = async () => { try { + // STEP 1: Bootstrap configuration files (copy .example.jsonc if needed) + log('main', 'BOOTSTRAP', 'Checking configuration files...', 'log', 'cyan') + const createdFiles = FileBootstrap.bootstrap() + + if (createdFiles.length > 0) { + FileBootstrap.displayStartupMessage(createdFiles) + + // If accounts file was just created, it will be empty + // User needs to configure before running + if (createdFiles.includes('Accounts')) { + log('main', 'BOOTSTRAP', 'Please configure your accounts in src/accounts.jsonc before running the bot.', 'warn', 'yellow') + process.exit(0) + } + } + // Check for updates BEFORE initializing and running tasks const updateMarkerPath = path.join(process.cwd(), '.update-happened') const isDocker = isDockerEnvironment() diff --git a/src/util/core/FileBootstrap.ts b/src/util/core/FileBootstrap.ts new file mode 100644 index 0000000..58b06c4 --- /dev/null +++ b/src/util/core/FileBootstrap.ts @@ -0,0 +1,116 @@ +import fs from 'fs' +import path from 'path' + +/** + * Bootstrap configuration files on startup + * Automatically copies .example.jsonc files to .jsonc if they don't exist + * + * This ensures first-time users have working config/accounts files + * without manual renaming steps + */ +export class FileBootstrap { + private static readonly FILES_TO_BOOTSTRAP = [ + { + example: 'src/config.example.jsonc', + target: 'src/config.jsonc', + name: 'Configuration' + }, + { + example: 'src/accounts.example.jsonc', + target: 'src/accounts.jsonc', + name: 'Accounts' + } + ] + + /** + * Bootstrap all necessary files + * @returns Array of files that were created + */ + public static bootstrap(): string[] { + const created: string[] = [] + + for (const file of this.FILES_TO_BOOTSTRAP) { + if (this.bootstrapFile(file.example, file.target, file.name)) { + created.push(file.name) + } + } + + return created + } + + /** + * Bootstrap a single file + * @returns true if file was created, false if it already existed + */ + private static bootstrapFile(examplePath: string, targetPath: string, name: string): boolean { + const rootDir = process.cwd() + const exampleFullPath = path.join(rootDir, examplePath) + const targetFullPath = path.join(rootDir, targetPath) + + // Check if target already exists + if (fs.existsSync(targetFullPath)) { + return false + } + + // Check if example exists + if (!fs.existsSync(exampleFullPath)) { + console.warn(`⚠️ Example file not found: ${examplePath}`) + return false + } + + try { + // Copy example to target + fs.copyFileSync(exampleFullPath, targetFullPath) + console.log(`βœ… Created ${name} file: ${targetPath}`) + return true + } catch (error) { + console.error(`❌ Failed to create ${name} file:`, error instanceof Error ? error.message : String(error)) + return false + } + } + + /** + * Check if all required files exist + * @returns true if all files exist + */ + public static checkFiles(): { allExist: boolean; missing: string[] } { + const missing: string[] = [] + const rootDir = process.cwd() + + for (const file of this.FILES_TO_BOOTSTRAP) { + const targetFullPath = path.join(rootDir, file.target) + if (!fs.existsSync(targetFullPath)) { + missing.push(file.name) + } + } + + return { + allExist: missing.length === 0, + missing + } + } + + /** + * Display startup message if files were bootstrapped + */ + public static displayStartupMessage(createdFiles: string[]): void { + if (createdFiles.length === 0) { + return + } + + console.log('\n' + '='.repeat(70)) + console.log('πŸŽ‰ FIRST-TIME SETUP COMPLETE') + console.log('='.repeat(70)) + console.log('\nThe following files have been created for you:') + + for (const fileName of createdFiles) { + console.log(` βœ“ ${fileName}`) + } + + console.log('\nπŸ“ NEXT STEPS:') + console.log(' 1. Edit src/accounts.jsonc to add your Microsoft accounts') + console.log(' 2. (Optional) Customize src/config.jsonc settings') + console.log(' 3. Run the bot again with: npm start') + console.log('\n' + '='.repeat(70) + '\n') + } +}