From 6b687a1018add4b8b82775b8cd6a8d13d48461d7 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sat, 8 Nov 2025 18:25:51 +0100 Subject: [PATCH] Remove BuyMode and fix --- README.md | 26 ----- docs/FAQ.md | 6 - docs/buy-mode.md | 217 ------------------------------------ docs/config-reference.md | 3 +- docs/config.md | 1 - docs/docker.md | 14 --- docs/index.md | 1 - docs/ntfy.md | 3 +- package.json | 1 - src/browser/Browser.ts | 93 +--------------- src/config.jsonc | 5 - src/flows/BuyModeManual.ts | 197 -------------------------------- src/index.ts | 223 ++++--------------------------------- src/interface/Config.ts | 6 - src/util/BuyMode.ts | 219 ------------------------------------ src/util/Load.ts | 6 - 16 files changed, 24 insertions(+), 997 deletions(-) delete mode 100644 docs/buy-mode.md delete mode 100644 src/flows/BuyModeManual.ts delete mode 100644 src/util/BuyMode.ts diff --git a/README.md b/README.md index 7f28017..7f51f6a 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ For detailed configuration, advanced features, and troubleshooting, visit our co | **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide | | **[Configuration](docs/config.md)** | Complete configuration options reference | | **[Accounts & 2FA](docs/accounts.md)** | Setting up accounts with TOTP authentication | -| **[Buy Mode](docs/buy-mode.md)** | πŸ’³ Manual purchase monitoring with point tracking | | **[Dashboard](src/dashboard/README.md)** | πŸ†• Local web dashboard for monitoring and control | | **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation | | **[Docker Deployment](docs/docker.md)** | Running in containers | @@ -127,29 +126,6 @@ Access at `http://localhost:3000` to: --- -## πŸ’³ Buy Mode - -Monitor your points in real-time while manually redeeming rewards: - -```bash -# Interactive account selection (recommended) -npm run buy - -# Or specify account directly -npm run buy your@email.com -npm run buy 1 # By account number -``` - -**What it does:** -- Opens browser with 2 tabs: monitoring + your browsing tab -- Tracks point changes every ~10 seconds -- Sends alerts when points are spent -- Completely passive - you control the redemption - -**[πŸ“– Full Buy Mode Guide](docs/buy-mode.md)** - ---- - ## πŸ†• Account Creator Automatically create new Microsoft accounts with referral link support: @@ -231,8 +207,6 @@ Container includes: - βœ… Random execution delays (anti-detection) - βœ… Health checks -**⚠️ Note:** Buy Mode is not available in Docker (requires interactive terminal) - **πŸ“– [Full Docker Guide](docs/docker.md)** --- diff --git a/docs/FAQ.md b/docs/FAQ.md index bcf0016..56b0131 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -265,12 +265,6 @@ This creates diverse, natural-looking search patterns. See [Query Diversity Engine](config.md#query-diversity-engine). -### What is "Buy Mode"? - -A manual purchase assistant that monitors your points in real-time while you redeem rewards. Not fully automatedβ€”you control the redemption. - -See [Buy Mode Guide](buy-mode.md). - ### Can I get notifications? Yes! The script supports: diff --git a/docs/buy-mode.md b/docs/buy-mode.md deleted file mode 100644 index 68455bd..0000000 --- a/docs/buy-mode.md +++ /dev/null @@ -1,217 +0,0 @@ -# πŸ’³ Buy Mode - -**Manually redeem rewards while monitoring points** - ---- - -## πŸ’‘ What Is It? - -Launches browser and **passively monitors** your points balance while you manually shop/redeem. - -**Use case:** Safely redeem gift cards without automation interference. - ---- - -## ⚑ Quick Start - -### Option 1: Interactive Selection (Recommended) -```bash -npm run buy -``` - -### Option 2: Direct Email Selection -```bash -npm run buy your@email.com -``` - -### Option 3: Numeric Account Index -```bash -npm run buy 1 -``` - -**What happens:** -1. Opens 2 browser tabs: - - **Monitor tab** β€” Background point tracking (auto-refresh every ~10s) - - **Your tab** β€” Use this for manual purchases -2. Monitors points passively without clicking -3. Sends alerts when spending detected -4. Session runs for configured duration (default: 45 minutes) - ---- - -## 🎯 Account Selection Methods - -### 1. Interactive Selection (Easiest) -Run without arguments to see a menu of available accounts: -```bash -npm run buy -``` - -You'll see: -``` -Available accounts: -──────────────────────────────────────────────────────────── -[1] my***t@outlook.com Direct -[2] se***d@outlook.com πŸ”’ Proxy -[3] th***d@outlook.com Direct -──────────────────────────────────────────────────────────── - -Enter account number (1-3) or email: -``` - -**Features:** -- βœ… Masked emails for privacy -- βœ… Proxy status indication -- βœ… Only shows enabled accounts -- βœ… Validates selection before proceeding - -### 2. By Email Address -Specify the exact email address: -```bash -npm run buy myaccount@outlook.com -``` - -### 3. By Account Number -Use the account index (1-based): -```bash -npm run buy 1 # First enabled account -npm run buy 2 # Second enabled account -``` - ---- - -## 🎯 Example Usage - -### Redeem Gift Card (Interactive) - -```bash -npm run buy -``` -Choose your account from the interactive menu. - -### Redeem Gift Card (Email) - -```bash -npm run buy myaccount@outlook.com -``` - -1. Script opens Microsoft Rewards in browser -2. Use the **user tab** to browse and redeem -3. **Monitor tab** tracks your balance in background -4. Get notification when points decrease - -### Redeem Gift Card (Index) - -```bash -npm run buy 1 -``` -Directly selects the first enabled account from your accounts list. - ---- - -## βš™οΈ Configuration - -**Set max session time:** - -**Edit** `src/config.jsonc`: -```jsonc -{ - "buyMode": { - "maxMinutes": 45 // Session duration (minimum: 10, default: 45) - } -} -``` - -**Session behavior:** -- Monitor tab refreshes every ~10 seconds -- Session automatically ends after `maxMinutes` -- You can close the browser anytime -- Cookies are saved at the end - ---- - -## πŸ”” Notifications - -Buy mode sends real-time alerts when: -- πŸ’³ **Points spent** β€” Shows amount and new balance -- πŸ“‰ **Balance changes** β€” Tracks cumulative spending - -**Example alert:** -``` -πŸ’³ Spend Detected (Buy Mode) -Account: user@email.com -Spent: -500 points -Current: 12,500 points -Session spent: 1,200 points -``` - -**Alert channels:** Uses your configured webhooks (Discord, NTFY, etc.) - ---- - -## πŸ› οΈ Troubleshooting - -| Problem | Solution | -|---------|----------| -| **"No enabled accounts found"** | Enable at least one account in `accounts.jsonc` | -| **"Invalid account index"** | Check your account number (must be 1-N) | -| **"Account not found"** | Verify email spelling and that account is enabled | -| **Monitor tab closes** | Script auto-reopens it in background | -| **No spending alerts** | Check webhook/NTFY config in `config.jsonc` | -| **Session too short** | Increase `maxMinutes` in config | -| **Interactive prompt not showing** | Run: `npm run buy` (no arguments) | - ---- - -## ⚠️ Important Notes - -- βœ… **Browser visible** β€” Always runs in visible mode (not headless) -- βœ… **No automation** β€” Script only monitors, never clicks or redeems -- βœ… **Safe** β€” Use your browsing tab normally -- βœ… **Real-time tracking** β€” Immediate notifications on point changes -- βœ… **Multiple selection methods** β€” Interactive, email, or index -- βœ… **Privacy-friendly** β€” Emails are masked in interactive mode -- ⚠️ **Only enabled accounts** β€” Disabled accounts don't appear - ---- - -## πŸ“Š Session Summary - -At the end of each buy mode session, you'll receive a summary: - -``` -Account: myaccount@outlook.com -Duration: 45m 12s -Initial points: 15,000 -Current points: 13,500 -Total spent: 1,500 -``` - -This summary is sent via your configured notification channels. - ---- - -## πŸ’‘ Pro Tips - -- **Use interactive mode** for the safest selection -- **Build first** if you modified code: `npm run build` -- **Multiple accounts?** Use numeric index for speed -- **Check your balance** before and after in the monitor tab - ---- - -## πŸ“š Next Steps - -**Setup notifications?** -β†’ **[Discord Webhooks](./conclusionwebhook.md)** -β†’ **[NTFY Push](./ntfy.md)** - -**Manage multiple accounts?** -β†’ **[Accounts Guide](./accounts.md)** - -**Back to automation?** -β†’ **[Getting Started](./getting-started.md)** - ---- - -**[← Back to Hub](./index.md)** | **[Config Guide](./config.md)** diff --git a/docs/config-reference.md b/docs/config-reference.md index a019c69..b62f244 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -123,11 +123,10 @@ This page mirrors the defaults that ship in `src/config.jsonc` and explains what --- -## Buy Mode & Updates +## Updates | Key | Default | Notes | | --- | --- | --- | -| `buyMode.maxMinutes` | `45` | Session length cap when using `-buy`. | | `update.git` | `true` | Run git updater after completion. | | `update.docker` | `false` | Use Docker updater instead. | | `update.scriptPath` | `setup/update/update.mjs` | Update script path. | diff --git a/docs/config.md b/docs/config.md index 35fc4f8..0a721c1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -53,7 +53,6 @@ When running inside Docker, you can instead rely on `update.docker: true` so the - **Risk management**: Leave `riskManagement.enabled` and `banPrediction` on unless you have a reason to reduce telemetry. Raising `riskThreshold` (>75) makes alerts rarer. - **Search pacing**: The delay window (`search.settings.delay.min` / `max`) accepts either numbers (ms) or strings like `"2min"`. Keep the range wide enough for natural behaviour. - **Dry run**: Set `dryRun: true` to test account rotation without performing tasks. Useful for validating login flow after configuration changes. -- **Buy mode**: The config entry simply caps the session length. Use `npm run buy` to launch it. --- diff --git a/docs/docker.md b/docs/docker.md index 3d82ef3..2f98521 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -201,20 +201,6 @@ The build process regenerates `package-lock.json` inside the container to ensure - **Native performance on all architectures** - **Raspberry Pi users won't encounter binary mismatch errors** -### Buy Mode Not Supported - -**Buy Mode cannot be used in Docker** because it requires interactive terminal input. Use Buy Mode only in local installations: - -```bash -# βœ… Works locally -npm run buy - -# ❌ Does not work in Docker -docker exec microsoft-rewards-script npm run buy -``` - -For manual redemptions, run the bot locally outside Docker. - ### Headless Mode Required Docker containers **must run in headless mode**. The Dockerfile automatically sets `FORCE_HEADLESS=1`. Do not disable this. diff --git a/docs/index.md b/docs/index.md index e69f82b..1f1e527 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,6 @@ | **[NTFY Alerts](ntfy.md)** | Mobile push notifications | | **[Proxy Setup](proxy.md)** | IP rotation (optional) | | **[Docker](docker.md)** | Container deployment | -| **[Buy Mode](buy-mode.md)** | Manual purchase monitoring | --- diff --git a/docs/ntfy.md b/docs/ntfy.md index e8fc909..d41b9dd 100644 --- a/docs/ntfy.md +++ b/docs/ntfy.md @@ -51,8 +51,7 @@ Open NTFY app β†’ Add subscription β†’ Enter your topic name - 🚨 **Errors** β€” Script crashes, login failures - ⚠️ **Warnings** β€” Missing points, suspicious activity - πŸ† **Milestones** β€” Account completed successfully -- πŸ’³ **Buy mode** β€” Point spending detected -- πŸ“Š **Summary** β€” End-of-run report +- **Summary** β€” End-of-run report --- diff --git a/package.json b/package.json index 8be88d6..dce5c8e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "start": "node --enable-source-maps ./dist/index.js", "ts-start": "node --loader ts-node/esm ./src/index.ts", "dev": "ts-node ./src/index.ts -dev", - "buy": "node --enable-source-maps ./dist/index.js -buy", "creator": "ts-node ./src/account-creation/cli.ts", "dashboard": "node --enable-source-maps ./dist/index.js -dashboard", "dashboard-dev": "ts-node ./src/index.ts -dashboard", diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index da46c42..f9ccbe4 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -27,20 +27,10 @@ class Browser { let browser: import('rebrowser-playwright').Browser try { const envForceHeadless = process.env.FORCE_HEADLESS === '1' - let headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false) - - // Buy/Interactive mode: always visible and with enhanced stealth - const isBuyMode = this.bot.isBuyModeEnabled() - if (isBuyMode && !envForceHeadless) { - if (headless !== false) { - const target = this.bot.getBuyModeTarget() - this.bot.log(this.bot.isMobile, 'BROWSER', `Interactive mode: forcing headless=false${target ? ` for ${target}` : ''}`, 'warn') - } - headless = false - } + const headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false) const engineName = 'chromium' - this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless}${isBuyMode ? ', stealth-mode=ENHANCED' : ''})`) + this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) const proxyConfig = this.buildPlaywrightProxy(proxy) const isLinux = process.platform === 'linux' @@ -63,26 +53,10 @@ class Browser { '--disk-cache-size=1' ] : [] - // ENHANCED STEALTH MODE for Buy/Interactive Mode - // These arguments help bypass CAPTCHA and automation detection - const stealthArgs = isBuyMode ? [ - '--disable-blink-features=AutomationControlled', // Critical: Hide automation - '--disable-features=IsolateOrigins,site-per-process', // Reduce detection surface - '--disable-site-isolation-trials', - '--disable-web-security', // Allow cross-origin (may help with CAPTCHA) - '--disable-features=VizDisplayCompositor', // Reduce GPU fingerprinting - '--no-first-run', - '--no-default-browser-check', - '--disable-infobars', - '--window-position=0,0', - '--window-size=1920,1080', // Consistent window size - '--start-maximized' - ] : [] - browser = await playwright.chromium.launch({ headless, ...(proxyConfig && { proxy: proxyConfig }), - args: [...baseArgs, ...linuxStabilityArgs, ...stealthArgs], + args: [...baseArgs, ...linuxStabilityArgs], timeout: isLinux ? 90000 : 60000 }) } catch (e: unknown) { @@ -106,8 +80,6 @@ class Browser { const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000 context.setDefaultTimeout(typeof globalTimeout === 'number' ? globalTimeout : this.bot.utils.stringToMs(globalTimeout)) - const isBuyMode = this.bot.isBuyModeEnabled() - try { context.on('page', async (page) => { try { @@ -131,65 +103,6 @@ class Browser { document.documentElement.appendChild(style) } catch {/* ignore */} }) - - // ENHANCED ANTI-DETECTION for Buy/Interactive Mode - if (isBuyMode) { - await page.addInitScript(` - // Override navigator.webdriver (critical for CAPTCHA bypass) - Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { - get: () => false - }); - - // Add chrome runtime (looks more human) - Object.defineProperty(window, 'chrome', { - writable: true, - enumerable: true, - configurable: false, - value: { runtime: {} } - }); - - // Add plugins (looks more human) - Object.defineProperty(navigator, 'plugins', { - get: () => [ - { name: 'Chrome PDF Plugin' }, - { name: 'Chrome PDF Viewer' }, - { name: 'Native Client' } - ] - }); - - // Languages - Object.defineProperty(navigator, 'languages', { - get: () => ['en-US', 'en', 'fr'] - }); - - // Hide automation markers - ['__nightmare', '__playwright', '__pw_manual', '__webdriver_script_fn', 'webdriver'].forEach(prop => { - try { - if (prop in window) delete (window as Record)[prop]; - } catch { - // Silently ignore: property deletion may be blocked by browser security - } - }); - - // Override permissions to avoid detection - const originalPermissionsQuery = window.navigator.permissions.query; - window.navigator.permissions.query = function(params) { - if (params.name === 'notifications') { - return Promise.resolve({ - state: Notification.permission, - name: 'notifications', - onchange: null, - addEventListener: () => {}, - removeEventListener: () => {}, - dispatchEvent: () => true - }); - } - return originalPermissionsQuery.call(this, params); - }; - `) - - this.bot.log(this.bot.isMobile, 'BROWSER', 'πŸ›‘οΈ Enhanced stealth mode activated (anti-CAPTCHA)', 'log', 'green') - } } catch (e) { this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') } diff --git a/src/config.jsonc b/src/config.jsonc index f7882e9..603ccec 100644 --- a/src/config.jsonc +++ b/src/config.jsonc @@ -143,11 +143,6 @@ "host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security) }, - // Buy mode - "buyMode": { - "maxMinutes": 45 - }, - // Updates "update": { "enabled": true, // Enable automatic updates (default: true) diff --git a/src/flows/BuyModeManual.ts b/src/flows/BuyModeManual.ts deleted file mode 100644 index 0907acc..0000000 --- a/src/flows/BuyModeManual.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Buy Mode Manual Handler Module - * Extracted from index.ts runBuyMode() method - * - * Provides manual spending mode where user retains control: - * - Opens two browser tabs (monitor + browsing) - * - Passively monitors point changes every ~10s - * - Detects spending and sends notifications - * - Session has configurable duration (default: 45 minutes) - * - User manually selects and purchases items - */ - -import type { BrowserContext } from 'playwright' -import type { MicrosoftRewardsBot } from '../index' -import type { Account } from '../interface/Account' -import { BuyModeMonitor, BuyModeSelector } from '../util/BuyMode' -import { saveSessionData } from '../util/Load' -import { log } from '../util/Logger' - -interface AccountSummary { - email: string - durationMs: number - desktopCollected: number - mobileCollected: number - totalCollected: number - initialTotal: number - endTotal: number - errors: string[] - banned: { status: boolean; reason: string } -} - -export class BuyModeManual { - private bot: MicrosoftRewardsBot - - constructor(bot: MicrosoftRewardsBot) { - this.bot = bot - } - - /** - * Execute manual buy mode session - * Opens browser, logs in, and passively monitors points while user browses/purchases - * @param buyModeArgument Optional account email to use (otherwise prompts user) - * @returns Promise that resolves when session ends - */ - async execute(buyModeArgument?: string): Promise { - try { - const buyModeConfig = this.bot.config.buyMode as { maxMinutes?: number } | undefined - const maxMinutes = buyModeConfig?.maxMinutes ?? 45 - - // Access private accounts array via type assertion - const accounts = (this.bot as unknown as { accounts: Account[] }).accounts - const selector = new BuyModeSelector(accounts) - const selection = await selector.selectAccount(buyModeArgument, maxMinutes) - - if (!selection) { - log('main', 'BUY-MODE', 'Buy mode cancelled: no account selected', 'warn') - return - } - - const { account, maxMinutes: sessionMaxMinutes } = selection - - log('main', 'BUY-MODE', `Buy mode ENABLED for ${account.email}. Opening 2 tabs: (1) monitor tab (auto-refresh), (2) your browsing tab`, 'log', 'green') - log('main', 'BUY-MODE', `Session duration: ${sessionMaxMinutes} minutes. Monitor tab refreshes every ~10s. Use the other tab for your actions.`, 'log', 'yellow') - - this.bot.isMobile = false - - // Access private browserFactory via type assertion - const browserFactory = (this.bot as unknown as { browserFactory: { createBrowser: (proxy: Account['proxy'], email: string) => Promise } }).browserFactory - const browser = await browserFactory.createBrowser(account.proxy, account.email) - - // Open the monitor tab FIRST so auto-refresh happens out of the way - let monitor = await browser.newPage() - - // Access private login via type assertion (use 'any' for internal page type - unavoidable) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const loginModule = (this.bot as any).login - await loginModule.login(monitor, account.email, account.password, account.totp) - await this.bot.browser.func.goHome(monitor) - this.bot.log(false, 'BUY-MODE', 'Opened MONITOR tab (auto-refreshes to track points).', 'log', 'yellow') - - // Then open the user free-browsing tab SECOND so users don't see the refreshes - const page = await browser.newPage() - await this.bot.browser.func.goHome(page) - this.bot.log(false, 'BUY-MODE', 'Opened USER tab (use this one to redeem/purchase freely).', 'log', 'green') - - // Helper to recreate monitor tab if the user closes it - const recreateMonitor = async () => { - try { if (!monitor.isClosed()) await monitor.close() } catch { /* ignore */ } - monitor = await browser.newPage() - await this.bot.browser.func.goHome(monitor) - } - - // Helper to send an immediate spend notice via webhooks/NTFY - const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => { - try { - const { ConclusionWebhook } = await import('../util/ConclusionWebhook') - await ConclusionWebhook( - this.bot.config, - 'πŸ’³ Spend Detected', - `**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`, - undefined, - 0xFFAA00 - ) - } catch (e) { - this.bot.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn') - } - } - - // Get initial points - let initial = 0 - try { - const data = await this.bot.browser.func.getDashboardData(monitor) - initial = data.userStatus.availablePoints || 0 - } catch {/* ignore */} - - const pointMonitor = new BuyModeMonitor(initial) - - this.bot.log(false, 'BUY-MODE', `Logged in as ${account.email}. Starting passive point monitoring (session: ${sessionMaxMinutes} min)`) - - // Passive watcher: poll points periodically without clicking. - const start = Date.now() - const endAt = start + sessionMaxMinutes * 60 * 1000 - - while (Date.now() < endAt) { - await this.bot.utils.wait(10000) - - // If monitor tab was closed by user, recreate it quietly - try { - if (monitor.isClosed()) { - this.bot.log(false, 'BUY-MODE', 'Monitor tab was closed; reopening in background...', 'warn') - await recreateMonitor() - } - } catch (e) { - this.bot.log(false, 'BUY-MODE', `Failed to check/recreate monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - - try { - const data = await this.bot.browser.func.getDashboardData(monitor) - const nowPts = data.userStatus.availablePoints || 0 - - const spendInfo = pointMonitor.checkSpending(nowPts) - if (spendInfo) { - this.bot.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`) - await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total) - } - } catch (err) { - // If we lost the page context, recreate the monitor tab and continue - const msg = err instanceof Error ? err.message : String(err) - if (/Target closed|page has been closed|browser has been closed/i.test(msg)) { - this.bot.log(false, 'BUY-MODE', 'Monitor page closed or lost; recreating...', 'warn') - try { - await recreateMonitor() - } catch (e) { - this.bot.log(false, 'BUY-MODE', `Failed to recreate monitor: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - } else { - this.bot.log(false, 'BUY-MODE', `Dashboard check error: ${msg}`, 'warn') - } - } - } - - // Save cookies and close monitor; keep main page open for user until they close it themselves - try { - await saveSessionData(this.bot.config.sessionPath, browser, account.email, this.bot.isMobile) - } catch (e) { - log(false, 'BUY-MODE', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - try { - if (!monitor.isClosed()) await monitor.close() - } catch (e) { - log(false, 'BUY-MODE', `Failed to close monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - - // Send a final minimal conclusion webhook for this manual session - const monitorSummary = pointMonitor.getSummary() - const summary: AccountSummary = { - email: account.email, - durationMs: monitorSummary.duration, - desktopCollected: 0, - mobileCollected: 0, - totalCollected: -monitorSummary.spent, // negative indicates spend - initialTotal: monitorSummary.initial, - endTotal: monitorSummary.current, - errors: [], - banned: { status: false, reason: '' } - } - - // Access private sendConclusion via type assertion - const sendConclusion = (this.bot as unknown as { sendConclusion: (summaries: AccountSummary[]) => Promise }).sendConclusion - await sendConclusion.call(this.bot, [summary]) - - this.bot.log(false, 'BUY-MODE', 'Buy mode session finished (monitoring period ended). You can close the browser when done.') - } catch (e) { - this.bot.log(false, 'BUY-MODE', `Error in buy mode: ${e instanceof Error ? e.message : String(e)}`, 'error') - } - } -} diff --git a/src/index.ts b/src/index.ts index d1fd0e6..dde99b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,10 @@ -// ------------------------------- -// REFACTORING STATUS: COMPLETED βœ… +ο»Ώ// ------------------------------- +// REFACTORING STATUS: COMPLETED Òœ… // ------------------------------- // Successfully modularized into separate flow modules: -// βœ… DesktopFlow.ts (Desktop automation logic) - INTEGRATED -// βœ… MobileFlow.ts (Mobile automation logic) - INTEGRATED -// βœ… SummaryReporter.ts (Report generation) - INTEGRATED -// βœ… BuyModeManual.ts (Manual spending mode) - CREATED (integration pending) +// Òœ… DesktopFlow.ts (Desktop automation logic) - INTEGRATED +// Òœ… MobileFlow.ts (Mobile automation logic) - INTEGRATED +// Òœ… SummaryReporter.ts (Report generation) - INTEGRATED // This improved testability and maintainability by 31% code reduction. // ------------------------------- @@ -17,16 +16,14 @@ import path from 'path' import type { Page } from 'playwright' import { createInterface } from 'readline' -import Browser from './browser/Browser' import BrowserFunc from './browser/BrowserFunc' import BrowserUtil from './browser/BrowserUtil' import Axios from './util/Axios' import { detectBanReason } from './util/BanDetector' -import { BuyModeMonitor, BuyModeSelector } from './util/BuyMode' import Humanizer from './util/Humanizer' import JobState from './util/JobState' -import { loadAccounts, loadConfig, saveSessionData } from './util/Load' +import { loadAccounts, loadConfig } from './util/Load' import { log } from './util/Logger' import { MobileRetryTracker } from './util/MobileRetryTracker' import { QueryDiversityEngine } from './util/QueryDiversityEngine' @@ -34,7 +31,6 @@ import { StartupValidator } from './util/StartupValidator' import { Util } from './util/Utils' import { Activities } from './functions/Activities' -import { Login } from './functions/Login' import { Workers } from './functions/Workers' import { DesktopFlow } from './flows/DesktopFlow' @@ -65,12 +61,8 @@ export class MicrosoftRewardsBot { public compromisedReason?: string private activeWorkers: number - private browserFactory: Browser = new Browser(this) private accounts: Account[] public workers: Workers // Made public for DesktopFlow access - private login = new Login(this) - private buyModeEnabled: boolean = false - private buyModeArgument?: string // Summary collection (per process) private accountSummaries: AccountSummary[] = [] @@ -107,27 +99,6 @@ export class MicrosoftRewardsBot { cacheMinutes: this.config.queryDiversity.cacheMinutes }) } - - // Buy mode: CLI args take precedence over config - const idx = process.argv.indexOf('-buy') - if (idx >= 0) { - this.buyModeEnabled = true - this.buyModeArgument = process.argv[idx + 1] - } else { - // Fallback to config if no CLI flag - const buyModeConfig = this.config.buyMode as { enabled?: boolean } | undefined - if (buyModeConfig?.enabled === true) { - this.buyModeEnabled = true - } - } - } - - public isBuyModeEnabled(): boolean { - return this.buyModeEnabled === true - } - - public getBuyModeTarget(): string | undefined { - return this.buyModeArgument } async initialize() { @@ -214,7 +185,7 @@ export class MicrosoftRewardsBot { }) return new Promise((resolve) => { - rl.question('\n⚠️ Reset job state and run all accounts again? (y/N): ', (answer) => { + rl.question('\nÒő ï¸ Reset job state and run all accounts again? (y/N): ', (answer) => { rl.close() const trimmed = answer.trim().toLowerCase() resolve(trimmed === 'y' || trimmed === 'yes') @@ -242,12 +213,6 @@ export class MicrosoftRewardsBot { this.printBanner() log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`) - // If buy mode is enabled, run single-account interactive session without automation - if (this.buyModeEnabled) { - await this.runBuyMode() - return - } - // Only cluster when there's more than 1 cluster demanded if (this.config.clusters > 1) { if (cluster.isPrimary) { @@ -269,170 +234,20 @@ export class MicrosoftRewardsBot { } } } - - /** Manual spending session: login, then leave control to user while we passively monitor points. */ - private async runBuyMode() { - try { - await this.initialize() - - const buyModeConfig = this.config.buyMode as { maxMinutes?: number } | undefined - const maxMinutes = buyModeConfig?.maxMinutes ?? 45 - - const selector = new BuyModeSelector(this.accounts) - const selection = await selector.selectAccount(this.buyModeArgument, maxMinutes) - - if (!selection) { - log('main', 'BUY-MODE', 'Buy mode cancelled: no account selected', 'warn') - return - } - - const { account, maxMinutes: sessionMaxMinutes } = selection - - log('main', 'BUY-MODE', `Buy mode ENABLED for ${account.email}. Opening 2 tabs: (1) monitor tab (auto-refresh), (2) your browsing tab`, 'log', 'green') - log('main', 'BUY-MODE', `Session duration: ${sessionMaxMinutes} minutes. Monitor tab refreshes every ~10s. Use the other tab for your actions.`, 'log', 'yellow') - - this.isMobile = false - this.axios = new Axios(account.proxy) - const browser = await this.browserFactory.createBrowser(account.proxy, account.email) - // Open the monitor tab FIRST so auto-refresh happens out of the way - let monitor = await browser.newPage() - await this.login.login(monitor, account.email, account.password, account.totp) - await this.browser.func.goHome(monitor) - this.log(false, 'BUY-MODE', 'Opened MONITOR tab (auto-refreshes to track points).', 'log', 'yellow') - - // Then open the user free-browsing tab SECOND so users don’t see the refreshes - const page = await browser.newPage() - await this.browser.func.goHome(page) - this.log(false, 'BUY-MODE', 'Opened USER tab (use this one to redeem/purchase freely).', 'log', 'green') - - // Helper to recreate monitor tab if the user closes it - const recreateMonitor = async () => { - try { if (!monitor.isClosed()) await monitor.close() } catch { /* ignore */ } - monitor = await browser.newPage() - await this.browser.func.goHome(monitor) - } - - // Helper to send an immediate spend notice via webhooks/NTFY - const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => { - try { - const { ConclusionWebhook } = await import('./util/ConclusionWebhook') - await ConclusionWebhook( - this.config, - 'πŸ’³ Spend Detected', - `**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`, - undefined, - 0xFFAA00 - ) - } catch (e) { - this.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn') - } - } - - // Get initial points - let initial = 0 - try { - const data = await this.browser.func.getDashboardData(monitor) - initial = data.userStatus.availablePoints || 0 - } catch {/* ignore */} - - const pointMonitor = new BuyModeMonitor(initial) - - this.log(false, 'BUY-MODE', `Logged in as ${account.email}. Starting passive point monitoring (session: ${sessionMaxMinutes} min)`) - - // Passive watcher: poll points periodically without clicking. - const start = Date.now() - const endAt = start + sessionMaxMinutes * 60 * 1000 - - while (Date.now() < endAt) { - await this.utils.wait(10000) - - // If monitor tab was closed by user, recreate it quietly - try { - if (monitor.isClosed()) { - this.log(false, 'BUY-MODE', 'Monitor tab was closed; reopening in background...', 'warn') - await recreateMonitor() - } - } catch (e) { - this.log(false, 'BUY-MODE', `Failed to check/recreate monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - - try { - const data = await this.browser.func.getDashboardData(monitor) - const nowPts = data.userStatus.availablePoints || 0 - - const spendInfo = pointMonitor.checkSpending(nowPts) - if (spendInfo) { - this.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`) - await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total) - } - } catch (err) { - // If we lost the page context, recreate the monitor tab and continue - const msg = err instanceof Error ? err.message : String(err) - if (/Target closed|page has been closed|browser has been closed/i.test(msg)) { - this.log(false, 'BUY-MODE', 'Monitor page closed or lost; recreating...', 'warn') - try { - await recreateMonitor() - } catch (e) { - this.log(false, 'BUY-MODE', `Failed to recreate monitor: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - } else { - this.log(false, 'BUY-MODE', `Dashboard check error: ${msg}`, 'warn') - } - } - } - - // Save cookies and close monitor; keep main page open for user until they close it themselves - try { - await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile) - } catch (e) { - log(false, 'BUY-MODE', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - try { - if (!monitor.isClosed()) await monitor.close() - } catch (e) { - log(false, 'BUY-MODE', `Failed to close monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn') - } - - // Send a final minimal conclusion webhook for this manual session - const monitorSummary = pointMonitor.getSummary() - const summary: AccountSummary = { - email: account.email, - durationMs: monitorSummary.duration, - desktopCollected: 0, - mobileCollected: 0, - totalCollected: -monitorSummary.spent, // negative indicates spend - initialTotal: monitorSummary.initial, - endTotal: monitorSummary.current, - errors: [], - banned: { status: false, reason: '' } - } - await this.sendConclusion([summary]) - - this.log(false, 'BUY-MODE', 'Buy mode session finished (monitoring period ended). You can close the browser when done.') - } catch (e) { - this.log(false, 'BUY-MODE', `Error in buy mode: ${e instanceof Error ? e.message : String(e)}`, 'error') - } - } - private printBanner() { if (this.config.clusters > 1 && !cluster.isPrimary) return const version = this.getVersion() - const mode = this.buyModeEnabled ? 'Manual Mode' : 'Automated Mode' - log('main', 'BANNER', `Microsoft Rewards Bot v${version} - ${mode}`) + log('main', 'BANNER', `Microsoft Rewards Bot v${version}`) log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`) - if (this.buyModeEnabled) { - log('main', 'BANNER', `Target: ${this.buyModeArgument || 'Interactive selection'}`) - } else { - const upd = this.config.update || {} - const updTargets: string[] = [] - if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`) - if (upd.docker) updTargets.push('Docker') - if (updTargets.length > 0) { - log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`) - } + const upd = this.config.update || {} + const updTargets: string[] = [] + if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`) + if (upd.docker) updTargets.push('Docker') + if (updTargets.length > 0) { + log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`) } } @@ -540,7 +355,7 @@ export class MicrosoftRewardsBot { try { const updateCode = await this.runAutoUpdate() if (updateCode === 0) { - log('main', 'UPDATE', 'βœ… Update successful - next run will use new version', 'log', 'green') + log('main', 'UPDATE', 'Òœ… Update successful - next run will use new version', 'log', 'green') } } catch (e) { log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn') @@ -812,7 +627,7 @@ export class MicrosoftRewardsBot { // If update was successful (code 0), restart the script to use the new version // This is critical for cron jobs - they need to apply updates immediately if (updateResult === 0) { - log('main', 'UPDATE', 'βœ… Update successful - restarting with new version...', 'log', 'green') + log('main', 'UPDATE', 'Òœ… Update successful - restarting with new version...', 'log', 'green') // On Raspberry Pi/Linux with cron, just exit - cron will handle next run // No need to restart immediately, next scheduled run will use new code log('main', 'UPDATE', 'Next scheduled run will use the updated code', 'log') @@ -829,7 +644,7 @@ export class MicrosoftRewardsBot { const { ConclusionWebhook } = await import('./util/ConclusionWebhook') await ConclusionWebhook( this.config, - '🚫 Ban Detected', + 'Γ°ΕΈΕ‘Β« Ban Detected', `**Account:** ${email}\n**Reason:** ${reason || 'detected by heuristics'}`, undefined, DISCORD.COLOR_RED @@ -968,7 +783,7 @@ export class MicrosoftRewardsBot { const { ConclusionWebhook } = await import('./util/ConclusionWebhook') await ConclusionWebhook( this.config, - '🚨 Critical Security Alert', + 'Γ°ΕΈΕ‘Β¨ Critical Security Alert', `@everyone\n\n**Account:** ${email}\n**Issue:** ${reason}\n**Status:** All accounts paused pending review`, undefined, DISCORD.COLOR_RED @@ -1100,4 +915,4 @@ if (require.main === module) { log('main', 'MAIN-ERROR', `Error running bots: ${error}`, 'error') process.exit(1) }) -} \ No newline at end of file +} diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 91ca44a..0da5a8d 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -23,7 +23,6 @@ export interface Config { ntfy: ConfigNtfy; update?: ConfigUpdate; passesPerRun?: number; - buyMode?: ConfigBuyMode; // Optional manual spending mode vacation?: ConfigVacation; // Optional monthly contiguous off-days crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction @@ -90,11 +89,6 @@ export interface ConfigUpdate { autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials) } -export interface ConfigBuyMode { - enabled?: boolean; // if true, force buy mode session - maxMinutes?: number; // session duration cap -} - export interface ConfigVacation { enabled?: boolean; // default false minDays?: number; // default 3 diff --git a/src/util/BuyMode.ts b/src/util/BuyMode.ts deleted file mode 100644 index 217baf7..0000000 --- a/src/util/BuyMode.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { createInterface } from 'readline' -import type { Account } from '../interface/Account' -import { log } from './Logger' - -export interface BuyModeSelection { - account: Account - maxMinutes: number -} - -export class BuyModeSelector { - private accounts: Account[] - - constructor(accounts: Account[]) { - this.accounts = accounts.filter(acc => acc.enabled !== false) - } - - /** - * Parse the buy mode argument from CLI. - * Supports: email, numeric index (1-based), or undefined for interactive selection. - */ - async selectAccount( - argument?: string, - maxMinutes: number = 45 - ): Promise { - if (this.accounts.length === 0) { - log('main', 'BUY-MODE', 'No enabled accounts found. Please enable at least one account in accounts.jsonc', 'error') - return null - } - - let selectedAccount: Account | null = null - - if (!argument) { - selectedAccount = await this.promptInteractiveSelection() - } else if (this.isNumericIndex(argument)) { - selectedAccount = this.selectByIndex(argument) - } else if (this.isEmail(argument)) { - selectedAccount = this.selectByEmail(argument) - } else { - log('main', 'BUY-MODE', `Invalid argument: "${argument}". Expected email or numeric index.`, 'error') - return null - } - - if (!selectedAccount) { - return null - } - - return { - account: selectedAccount, - maxMinutes: Math.max(10, maxMinutes) - } - } - - private isNumericIndex(value: string): boolean { - return /^\d+$/.test(value) - } - - private isEmail(value: string): boolean { - return /@/.test(value) - } - - private selectByIndex(indexStr: string): Account | null { - const index = parseInt(indexStr, 10) - - if (index < 1 || index > this.accounts.length) { - log('main', 'BUY-MODE', `Invalid account index: ${index}. Valid range: 1-${this.accounts.length}`, 'error') - this.displayAccountList() - return null - } - - const account = this.accounts[index - 1] - log('main', 'BUY-MODE', `Selected account #${index}: ${this.maskEmail(account!.email)}`, 'log', 'green') - return account! - } - - private selectByEmail(email: string): Account | null { - const account = this.accounts.find(acc => acc.email.toLowerCase() === email.toLowerCase()) - - if (!account) { - log('main', 'BUY-MODE', `Account not found: ${email}`, 'error') - this.displayAccountList() - return null - } - - log('main', 'BUY-MODE', `Selected account: ${this.maskEmail(account.email)}`, 'log', 'green') - return account - } - - private async promptInteractiveSelection(): Promise { - log('main', 'BUY-MODE', 'No account specified. Please select an account:', 'log', 'cyan') - this.displayAccountList() - - const rl = createInterface({ - input: process.stdin, - output: process.stdout - }) - - return new Promise((resolve) => { - rl.question('\nEnter account number (1-' + this.accounts.length + ') or email: ', (answer) => { - rl.close() - - const trimmed = answer.trim() - - if (!trimmed) { - log('main', 'BUY-MODE', 'No selection made. Exiting buy mode.', 'warn') - resolve(null) - return - } - - let selected: Account | null = null - - if (this.isNumericIndex(trimmed)) { - selected = this.selectByIndex(trimmed) - } else if (this.isEmail(trimmed)) { - selected = this.selectByEmail(trimmed) - } else { - log('main', 'BUY-MODE', `Invalid input: "${trimmed}". Expected number or email.`, 'error') - } - - resolve(selected) - }) - }) - } - - private displayAccountList(): void { - // Note: console.log is intentionally used here for interactive user prompts - // This is a CLI menu, not system logging - should go directly to stdout - console.log('\nAvailable accounts:') - console.log('─'.repeat(60)) - - this.accounts.forEach((acc, idx) => { - const num = `[${idx + 1}]`.padEnd(5) - const email = this.maskEmail(acc.email).padEnd(35) - const proxy = acc.proxy?.url ? 'πŸ”’ Proxy' : 'Direct' - console.log(`${num} ${email} ${proxy}`) - }) - - console.log('─'.repeat(60)) - } - - private maskEmail(email: string): string { - const [local, domain] = email.split('@') - if (!local || !domain) return email - - if (local.length <= 3) { - return `${local[0]}***@${domain}` - } - - const visibleStart = local.slice(0, 2) - const visibleEnd = local.slice(-1) - return `${visibleStart}***${visibleEnd}@${domain}` - } -} - -export class BuyModeMonitor { - private initialPoints: number = 0 - private lastPoints: number = 0 - private totalSpent: number = 0 - private monitorStartTime: number = 0 - - constructor(initialPoints: number) { - this.initialPoints = initialPoints - this.lastPoints = initialPoints - this.monitorStartTime = Date.now() - } - - /** - * Update the current points and detect spending. - * Returns spending info if points decreased, null otherwise. - */ - checkSpending(currentPoints: number): { spent: number; current: number; total: number } | null { - if (currentPoints < this.lastPoints) { - const spent = this.lastPoints - currentPoints - this.totalSpent += spent - this.lastPoints = currentPoints - - return { - spent, - current: currentPoints, - total: this.totalSpent - } - } - - if (currentPoints > this.lastPoints) { - this.lastPoints = currentPoints - } - - return null - } - - getTotalSpent(): number { - return this.totalSpent - } - - getSessionDuration(): number { - return Date.now() - this.monitorStartTime - } - - getCurrentPoints(): number { - return this.lastPoints - } - - getInitialPoints(): number { - return this.initialPoints - } - - getSummary(): { - initial: number - current: number - spent: number - duration: number - } { - return { - initial: this.initialPoints, - current: this.lastPoints, - spent: this.totalSpent, - duration: this.getSessionDuration() - } - } -} diff --git a/src/util/Load.ts b/src/util/Load.ts index 94bf59e..df4f4bf 100644 --- a/src/util/Load.ts +++ b/src/util/Load.ts @@ -145,11 +145,6 @@ function normalizeConfig(raw: unknown): Config { const conclusionWebhook = notifications.conclusionWebhook ?? n.conclusionWebhook ?? { enabled: false, url: '' } const ntfy = notifications.ntfy ?? n.ntfy ?? { enabled: false, url: '', topic: '', authToken: '' } - // Buy Mode - const buyMode = n.buyMode ?? {} - const buyModeEnabled = typeof buyMode.enabled === 'boolean' ? buyMode.enabled : false - const buyModeMax = typeof buyMode.maxMinutes === 'number' ? buyMode.maxMinutes : 45 - // Fingerprinting const saveFingerprint = (n.fingerprinting?.saveFingerprint ?? n.saveFingerprint) ?? { mobile: false, desktop: false } @@ -244,7 +239,6 @@ function normalizeConfig(raw: unknown): Config { update: n.update, passesPerRun: passesPerRun, vacation: n.vacation, - buyMode: { enabled: buyModeEnabled, maxMinutes: buyModeMax }, crashRecovery: n.crashRecovery || {}, riskManagement, dryRun,