mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
Remove BuyMode and fix
This commit is contained in:
26
README.md
26
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)**
|
||||
|
||||
---
|
||||
|
||||
@@ -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:
|
||||
|
||||
217
docs/buy-mode.md
217
docs/buy-mode.md
@@ -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)**
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/index.ts
223
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<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 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user