mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
feat: add Buy Mode functionality for manual purchase monitoring and point tracking
This commit is contained in:
24
README.md
24
README.md
@@ -88,6 +88,7 @@ For detailed configuration, advanced features, and troubleshooting, visit our co
|
|||||||
| **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide |
|
| **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide |
|
||||||
| **[Configuration](docs/config.md)** | Complete configuration options reference |
|
| **[Configuration](docs/config.md)** | Complete configuration options reference |
|
||||||
| **[Accounts & 2FA](docs/accounts.md)** | Setting up accounts with TOTP authentication |
|
| **[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 |
|
| **[Dashboard](src/dashboard/README.md)** | 🆕 Local web dashboard for monitoring and control |
|
||||||
| **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation |
|
| **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation |
|
||||||
| **[Docker Deployment](docs/docker.md)** | Running in containers |
|
| **[Docker Deployment](docs/docker.md)** | Running in containers |
|
||||||
@@ -126,6 +127,29 @@ 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)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ⏰ Automatic Scheduling
|
## ⏰ Automatic Scheduling
|
||||||
|
|
||||||
Configure automatic task scheduling directly from `config.jsonc` - **perfect for Raspberry Pi!**
|
Configure automatic task scheduling directly from `config.jsonc` - **perfect for Raspberry Pi!**
|
||||||
|
|||||||
139
docs/buy-mode.md
139
docs/buy-mode.md
@@ -14,25 +14,85 @@ Launches browser and **passively monitors** your points balance while you manual
|
|||||||
|
|
||||||
## ⚡ Quick Start
|
## ⚡ Quick Start
|
||||||
|
|
||||||
|
### Option 1: Interactive Selection (Recommended)
|
||||||
```bash
|
```bash
|
||||||
npm start -- -buy your@email.com
|
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:**
|
**What happens:**
|
||||||
1. Opens 2 browser tabs:
|
1. Opens 2 browser tabs:
|
||||||
- **Monitor tab** — Background point tracking (auto-refresh)
|
- **Monitor tab** — Background point tracking (auto-refresh every ~10s)
|
||||||
- **Your tab** — Use this for manual purchases
|
- **Your tab** — Use this for manual purchases
|
||||||
2. Monitors points every ~10 seconds
|
2. Monitors points passively without clicking
|
||||||
3. Alerts you when spending detected
|
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
|
## 🎯 Example Usage
|
||||||
|
|
||||||
### Redeem Gift Card
|
### Redeem Gift Card (Interactive)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start -- -buy myaccount@outlook.com
|
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
|
1. Script opens Microsoft Rewards in browser
|
||||||
@@ -40,6 +100,13 @@ npm start -- -buy myaccount@outlook.com
|
|||||||
3. **Monitor tab** tracks your balance in background
|
3. **Monitor tab** tracks your balance in background
|
||||||
4. Get notification when points decrease
|
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
|
## ⚙️ Configuration
|
||||||
@@ -50,47 +117,86 @@ npm start -- -buy myaccount@outlook.com
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
"buyMode": {
|
"buyMode": {
|
||||||
"enabled": false,
|
"maxMinutes": 45 // Session duration (minimum: 10, default: 45)
|
||||||
"maxMinutes": 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
|
## 🔔 Notifications
|
||||||
|
|
||||||
Buy mode sends alerts when:
|
Buy mode sends real-time alerts when:
|
||||||
- 💳 **Points spent** — Shows amount and new balance
|
- 💳 **Points spent** — Shows amount and new balance
|
||||||
- 📉 **Balance changes** — Tracks cumulative spending
|
- 📉 **Balance changes** — Tracks cumulative spending
|
||||||
|
|
||||||
**Example alert:**
|
**Example alert:**
|
||||||
```
|
```
|
||||||
💳 Spend detected (Buy Mode)
|
💳 Spend Detected (Buy Mode)
|
||||||
Account: user@email.com
|
Account: user@email.com
|
||||||
Spent: -500 points
|
Spent: -500 points
|
||||||
Current: 12,500 points
|
Current: 12,500 points
|
||||||
Session spent: 1,200 points
|
Session spent: 1,200 points
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Alert channels:** Uses your configured webhooks (Discord, NTFY, etc.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Troubleshooting
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
| Problem | Solution |
|
| Problem | Solution |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| **Monitor tab closes** | Script auto-reopens it |
|
| **"No enabled accounts found"** | Enable at least one account in `accounts.jsonc` |
|
||||||
| **No spending alerts** | Check webhook/NTFY config |
|
| **"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 |
|
| **Session too short** | Increase `maxMinutes` in config |
|
||||||
|
| **Interactive prompt not showing** | Run: `npm run buy` (no arguments) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Important Notes
|
## ⚠️ Important Notes
|
||||||
|
|
||||||
- ✅ **Browser visible** — Always runs in visible mode
|
- ✅ **Browser visible** — Always runs in visible mode (not headless)
|
||||||
- ✅ **No automation** — Script only monitors, never clicks
|
- ✅ **No automation** — Script only monitors, never clicks or redeems
|
||||||
- ✅ **Safe** — Use your browsing tab normally
|
- ✅ **Safe** — Use your browsing tab normally
|
||||||
- ✅ **Notifications** — Uses existing webhook/NTFY settings
|
- ✅ **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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -100,6 +206,9 @@ Session spent: 1,200 points
|
|||||||
→ **[Discord Webhooks](./conclusionwebhook.md)**
|
→ **[Discord Webhooks](./conclusionwebhook.md)**
|
||||||
→ **[NTFY Push](./ntfy.md)**
|
→ **[NTFY Push](./ntfy.md)**
|
||||||
|
|
||||||
|
**Manage multiple accounts?**
|
||||||
|
→ **[Accounts Guide](./accounts.md)**
|
||||||
|
|
||||||
**Back to automation?**
|
**Back to automation?**
|
||||||
→ **[Getting Started](./getting-started.md)**
|
→ **[Getting Started](./getting-started.md)**
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ 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.
|
- **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.
|
- **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.
|
- **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 start -- -buy [email]` to launch it.
|
- **Buy mode**: The config entry simply caps the session length. Use `npm run buy` to launch it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.51.0",
|
"version": "2.55.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.51.0",
|
"version": "2.55.0",
|
||||||
"license": "CC-BY-NC-SA-4.0",
|
"license": "CC-BY-NC-SA-4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-bot",
|
"name": "microsoft-rewards-bot",
|
||||||
"version": "2.51.0",
|
"version": "2.55.0",
|
||||||
"description": "Automate Microsoft Rewards points collection",
|
"description": "Automate Microsoft Rewards points collection",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"start": "node --enable-source-maps ./dist/index.js",
|
"start": "node --enable-source-maps ./dist/index.js",
|
||||||
"ts-start": "node --loader ts-node/esm ./src/index.ts",
|
"ts-start": "node --loader ts-node/esm ./src/index.ts",
|
||||||
"dev": "ts-node ./src/index.ts -dev",
|
"dev": "ts-node ./src/index.ts -dev",
|
||||||
|
"buy": "node --enable-source-maps ./dist/index.js -buy",
|
||||||
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
|
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
|
||||||
"dashboard-dev": "ts-node ./src/index.ts -dashboard",
|
"dashboard-dev": "ts-node ./src/index.ts -dashboard",
|
||||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||||
|
|||||||
83
src/index.ts
83
src/index.ts
@@ -20,6 +20,7 @@ import JobState from './util/JobState'
|
|||||||
import { StartupValidator } from './util/StartupValidator'
|
import { StartupValidator } from './util/StartupValidator'
|
||||||
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
||||||
import { SchedulerManager } from './util/SchedulerManager'
|
import { SchedulerManager } from './util/SchedulerManager'
|
||||||
|
import { BuyModeSelector, BuyModeMonitor } from './util/BuyMode'
|
||||||
|
|
||||||
import { Login } from './functions/Login'
|
import { Login } from './functions/Login'
|
||||||
import { Workers } from './functions/Workers'
|
import { Workers } from './functions/Workers'
|
||||||
@@ -53,7 +54,8 @@ export class MicrosoftRewardsBot {
|
|||||||
private accounts: Account[]
|
private accounts: Account[]
|
||||||
private workers: Workers
|
private workers: Workers
|
||||||
private login = new Login(this)
|
private login = new Login(this)
|
||||||
private buyMode: { enabled: boolean; email?: string } = { enabled: false }
|
private buyModeEnabled: boolean = false
|
||||||
|
private buyModeArgument?: string
|
||||||
|
|
||||||
// Summary collection (per process)
|
// Summary collection (per process)
|
||||||
private accountSummaries: AccountSummary[] = []
|
private accountSummaries: AccountSummary[] = []
|
||||||
@@ -94,25 +96,23 @@ export class MicrosoftRewardsBot {
|
|||||||
// Buy mode: CLI args take precedence over config
|
// Buy mode: CLI args take precedence over config
|
||||||
const idx = process.argv.indexOf('-buy')
|
const idx = process.argv.indexOf('-buy')
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const target = process.argv[idx + 1]
|
this.buyModeEnabled = true
|
||||||
this.buyMode = target && /@/.test(target)
|
this.buyModeArgument = process.argv[idx + 1]
|
||||||
? { enabled: true, email: target }
|
|
||||||
: { enabled: true }
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to config if no CLI flag
|
// Fallback to config if no CLI flag
|
||||||
const buyModeConfig = this.config.buyMode as { enabled?: boolean } | undefined
|
const buyModeConfig = this.config.buyMode as { enabled?: boolean } | undefined
|
||||||
if (buyModeConfig?.enabled === true) {
|
if (buyModeConfig?.enabled === true) {
|
||||||
this.buyMode.enabled = true
|
this.buyModeEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isBuyModeEnabled(): boolean {
|
public isBuyModeEnabled(): boolean {
|
||||||
return this.buyMode.enabled === true
|
return this.buyModeEnabled === true
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBuyModeTarget(): string | undefined {
|
public getBuyModeTarget(): string | undefined {
|
||||||
return this.buyMode.email
|
return this.buyModeArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
@@ -168,10 +168,7 @@ export class MicrosoftRewardsBot {
|
|||||||
log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`)
|
log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`)
|
||||||
|
|
||||||
// If buy mode is enabled, run single-account interactive session without automation
|
// If buy mode is enabled, run single-account interactive session without automation
|
||||||
if (this.buyMode.enabled) {
|
if (this.buyModeEnabled) {
|
||||||
const targetInfo = this.buyMode.email ? ` for ${this.buyMode.email}` : ''
|
|
||||||
log('main', 'BUY-MODE', `Buy mode ENABLED${targetInfo}. We'll open 2 tabs: (1) a monitor tab that auto-refreshes to track points, (2) your browsing tab to redeem/purchase freely.`, 'log', 'green')
|
|
||||||
log('main', 'BUY-MODE', 'The monitor tab may refresh every ~10s. Use the other tab for your actions; monitoring is passive and non-intrusive.', 'log', 'yellow')
|
|
||||||
await this.runBuyMode()
|
await this.runBuyMode()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -192,9 +189,22 @@ export class MicrosoftRewardsBot {
|
|||||||
private async runBuyMode() {
|
private async runBuyMode() {
|
||||||
try {
|
try {
|
||||||
await this.initialize()
|
await this.initialize()
|
||||||
const email = this.buyMode.email || (this.accounts[0]?.email)
|
|
||||||
const account = this.accounts.find(a => a.email === email) || this.accounts[0]
|
const buyModeConfig = this.config.buyMode as { maxMinutes?: number } | undefined
|
||||||
if (!account) throw new Error('No account available for buy mode')
|
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.isMobile = false
|
||||||
this.axios = new Axios(account.proxy)
|
this.axios = new Axios(account.proxy)
|
||||||
@@ -232,22 +242,21 @@ export class MicrosoftRewardsBot {
|
|||||||
this.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn')
|
this.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get initial points
|
||||||
let initial = 0
|
let initial = 0
|
||||||
try {
|
try {
|
||||||
const data = await this.browser.func.getDashboardData(monitor)
|
const data = await this.browser.func.getDashboardData(monitor)
|
||||||
initial = data.userStatus.availablePoints || 0
|
initial = data.userStatus.availablePoints || 0
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
|
|
||||||
this.log(false, 'BUY-MODE', `Logged in as ${account.email}. Buy mode is active: monitor tab auto-refreshes; user tab is free for your actions. We'll observe points passively.`)
|
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.
|
// Passive watcher: poll points periodically without clicking.
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
let last = initial
|
const endAt = start + sessionMaxMinutes * 60 * 1000
|
||||||
let spent = 0
|
|
||||||
|
|
||||||
const buyModeConfig = this.config.buyMode as { maxMinutes?: number } | undefined
|
|
||||||
const maxMinutes = Math.max(10, buyModeConfig?.maxMinutes ?? 45)
|
|
||||||
const endAt = start + maxMinutes * 60 * 1000
|
|
||||||
|
|
||||||
while (Date.now() < endAt) {
|
while (Date.now() < endAt) {
|
||||||
await this.utils.wait(10000)
|
await this.utils.wait(10000)
|
||||||
@@ -265,16 +274,11 @@ export class MicrosoftRewardsBot {
|
|||||||
try {
|
try {
|
||||||
const data = await this.browser.func.getDashboardData(monitor)
|
const data = await this.browser.func.getDashboardData(monitor)
|
||||||
const nowPts = data.userStatus.availablePoints || 0
|
const nowPts = data.userStatus.availablePoints || 0
|
||||||
if (nowPts < last) {
|
|
||||||
// Points decreased -> likely spent
|
const spendInfo = pointMonitor.checkSpending(nowPts)
|
||||||
const delta = last - nowPts
|
if (spendInfo) {
|
||||||
spent += delta
|
this.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`)
|
||||||
last = nowPts
|
await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total)
|
||||||
this.log(false, 'BUY-MODE', `Detected spend: -${delta} points (current: ${nowPts})`)
|
|
||||||
// Immediate spend notice
|
|
||||||
await sendSpendNotice(delta, nowPts, spent)
|
|
||||||
} else if (nowPts > last) {
|
|
||||||
last = nowPts
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If we lost the page context, recreate the monitor tab and continue
|
// If we lost the page context, recreate the monitor tab and continue
|
||||||
@@ -305,14 +309,15 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send a final minimal conclusion webhook for this manual session
|
// Send a final minimal conclusion webhook for this manual session
|
||||||
|
const monitorSummary = pointMonitor.getSummary()
|
||||||
const summary: AccountSummary = {
|
const summary: AccountSummary = {
|
||||||
email: account.email,
|
email: account.email,
|
||||||
durationMs: Date.now() - start,
|
durationMs: monitorSummary.duration,
|
||||||
desktopCollected: 0,
|
desktopCollected: 0,
|
||||||
mobileCollected: 0,
|
mobileCollected: 0,
|
||||||
totalCollected: -spent, // negative indicates spend
|
totalCollected: -monitorSummary.spent, // negative indicates spend
|
||||||
initialTotal: initial,
|
initialTotal: monitorSummary.initial,
|
||||||
endTotal: last,
|
endTotal: monitorSummary.current,
|
||||||
errors: [],
|
errors: [],
|
||||||
banned: { status: false, reason: '' }
|
banned: { status: false, reason: '' }
|
||||||
}
|
}
|
||||||
@@ -328,13 +333,13 @@ export class MicrosoftRewardsBot {
|
|||||||
if (this.config.clusters > 1 && !cluster.isPrimary) return
|
if (this.config.clusters > 1 && !cluster.isPrimary) return
|
||||||
|
|
||||||
const version = this.getVersion()
|
const version = this.getVersion()
|
||||||
const mode = this.buyMode.enabled ? 'Manual Mode' : 'Automated Mode'
|
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} - ${mode}`)
|
||||||
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`)
|
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`)
|
||||||
|
|
||||||
if (this.buyMode.enabled) {
|
if (this.buyModeEnabled) {
|
||||||
log('main', 'BANNER', `Target: ${this.buyMode.email || 'First account'}`)
|
log('main', 'BANNER', `Target: ${this.buyModeArgument || 'Interactive selection'}`)
|
||||||
} else {
|
} else {
|
||||||
const upd = this.config.update || {}
|
const upd = this.config.update || {}
|
||||||
const updTargets: string[] = []
|
const updTargets: string[] = []
|
||||||
|
|||||||
217
src/util/BuyMode.ts
Normal file
217
src/util/BuyMode.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
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 {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user