Remove BuyMode and fix

This commit is contained in:
2025-11-08 18:25:51 +01:00
parent 40dd9b9fd8
commit 6b687a1018
16 changed files with 24 additions and 997 deletions

View File

@@ -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)**
---

View File

@@ -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:

View File

@@ -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)**

View File

@@ -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. |

View File

@@ -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.
---

View File

@@ -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.

View File

@@ -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 |
---

View File

@@ -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
---

View File

@@ -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",

View File

@@ -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<string, unknown>)[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')
}

View File

@@ -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)

View File

@@ -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<void> {
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<BrowserContext> } }).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<void> }).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')
}
}
}

View File

@@ -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<boolean>((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 dont 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)
})
}
}

View File

@@ -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

View File

@@ -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<BuyModeSelection | null> {
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<Account | null> {
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<Account | null>((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()
}
}
}

View File

@@ -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,