mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-11 02:46:17 +00:00
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,13 +4,9 @@ node_modules/
|
|||||||
.vscode/
|
.vscode/
|
||||||
.github/
|
.github/
|
||||||
diagnostic/
|
diagnostic/
|
||||||
report/
|
|
||||||
accounts.json
|
accounts.json
|
||||||
accounts.jsonc
|
|
||||||
notes
|
notes
|
||||||
accounts.dev.json
|
accounts.dev.json
|
||||||
accounts.dev.jsonc
|
|
||||||
accounts.main.json
|
accounts.main.json
|
||||||
accounts.main.jsonc
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.playwright-chromium-installed
|
.playwright-chromium-installed
|
||||||
110
README.md
110
README.md
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -31,36 +31,13 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
---
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
<div align="center">
|
║ WHAT DOES THIS DO? ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
### 📌 **Update Notice**
|
|
||||||
|
|
||||||
Recent updates changed the structure of `config.jsonc` and `accounts.jsonc` files (including extensions).
|
|
||||||
|
|
||||||
**If you see Git conflicts during `git pull` on these files:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Delete and fresh clone
|
|
||||||
rm -rf Microsoft-Rewards-Script
|
|
||||||
git clone -b v2 https://github.com/TheNetsky/Microsoft-Rewards-Script.git
|
|
||||||
cd Microsoft-Rewards-Script
|
|
||||||
|
|
||||||
# Manually re-enter your settings in the new files
|
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ Don't copy old config files directly—structure has changed. Re-enter your credentials and preferences manually.
|
<div align="center">
|
||||||
|
|
||||||
This notice will remain for a few releases. Once we reach stable v2.5+, automatic updates will work smoothly again.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
## What Does This Do?
|
|
||||||
|
|
||||||
**Automate your Microsoft Rewards daily activities with intelligent browser automation.**
|
**Automate your Microsoft Rewards daily activities with intelligent browser automation.**
|
||||||
Complete searches, quizzes, and promotions automatically while mimicking natural human behavior.
|
Complete searches, quizzes, and promotions automatically while mimicking natural human behavior.
|
||||||
@@ -76,11 +53,16 @@ Complete searches, quizzes, and promotions automatically while mimicking natural
|
|||||||
| **Daily Set Tasks** | ~30-50 pts | 1-2 min |
|
| **Daily Set Tasks** | ~30-50 pts | 1-2 min |
|
||||||
| **Promotions & Punch Cards** | Variable | 30s-2min |
|
| **Promotions & Punch Cards** | Variable | 30s-2min |
|
||||||
| **📊 TOTAL AVERAGE** | **150-300+ pts** | **3-5 min** |
|
| **📊 TOTAL AVERAGE** | **150-300+ pts** | **3-5 min** |
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Quick Start
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ QUICK START ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
### **🚀 Automated Setup** (Recommended)
|
### **🚀 Automated Setup** (Recommended)
|
||||||
|
|
||||||
@@ -110,7 +92,7 @@ git clone -b v2 https://github.com/TheNetsky/Microsoft-Rewards-Script.git
|
|||||||
cd Microsoft-Rewards-Script
|
cd Microsoft-Rewards-Script
|
||||||
|
|
||||||
# 2. Configure accounts
|
# 2. Configure accounts
|
||||||
cp src/accounts.example.jsonc src/accounts.json
|
cp src/accounts.example.json src/accounts.json
|
||||||
# Edit accounts.json with your Microsoft credentials
|
# Edit accounts.json with your Microsoft credentials
|
||||||
|
|
||||||
# 3. Install & build
|
# 3. Install & build
|
||||||
@@ -122,7 +104,11 @@ npm start
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Intelligent Features
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ INTELLIGENT FEATURES ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -173,7 +159,11 @@ Pre-flight checks
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Usage Commands
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ USAGE COMMANDS ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run automation once
|
# Run automation once
|
||||||
@@ -194,7 +184,11 @@ npm start -- --dry-run
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Configuration
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ CONFIGURATION ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
Edit `src/config.jsonc` to customize behavior:
|
Edit `src/config.jsonc` to customize behavior:
|
||||||
|
|
||||||
@@ -226,7 +220,11 @@ Edit `src/config.jsonc` to customize behavior:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Core Features
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ CORE FEATURES ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -271,7 +269,11 @@ Edit `src/config.jsonc` to customize behavior:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Documentation
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ DOCUMENTATION ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -288,7 +290,11 @@ Edit `src/config.jsonc` to customize behavior:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Technical Architecture
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ TECHNICAL ARCHITECTURE ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -321,7 +327,11 @@ Edit `src/config.jsonc` to customize behavior:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Important Disclaimers
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ IMPORTANT DISCLAIMERS ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -355,7 +365,11 @@ This project is for **educational purposes only**.
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Contributors
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ CONTRIBUTORS ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -399,7 +413,11 @@ This project is for **educational purposes only**.
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Community & Support
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ COMMUNITY & SUPPORT ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -424,13 +442,11 @@ GitHub Issues are also available for documentation and tracking.
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
> 💡 **Looking for enhanced builds?** Community-maintained versions with faster updates and advanced features may be available. Ask in our Discord for more info.
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
</div>
|
║ LICENSE ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
<br>
|
```
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
# 📊 Discord Webhooks
|
# 📊 Discord Webhooks
|
||||||
|
|
||||||
**Get beautiful run summaries in Discord**
|
**Get run summaries in Discord**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 What Is It?
|
## 💡 What Is It?
|
||||||
|
|
||||||
Sends a **professional, rich embed** to your Discord server after each run with:
|
Sends a **rich embed** to your Discord server after each run with:
|
||||||
- 📊 **Total accounts processed** with success/warning/banned breakdown
|
- 📊 Total accounts processed
|
||||||
- 💎 **Points earned** — clear before/after comparison
|
- 💎 Points earned
|
||||||
- ⚡ **Performance metrics** — average points and execution time
|
- ⏱️ Execution time
|
||||||
- 📈 **Per-account breakdown** — detailed stats for each account
|
- ❌ Errors encountered
|
||||||
- 🎨 **Beautiful formatting** — color-coded status, emojis, and clean layout
|
|
||||||
- ⏱️ **Timestamp** and version info in footer
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -33,10 +31,7 @@ Sends a **professional, rich embed** to your Discord server after each run with:
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"conclusionWebhook": {
|
"conclusionWebhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://discord.com/api/webhooks/123456789/abcdef-your-webhook-token",
|
"url": "https://discord.com/api/webhooks/123456789/abcdef-your-webhook-token"
|
||||||
// Optional: Customize webhook appearance
|
|
||||||
"username": "Microsoft Rewards",
|
|
||||||
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,51 +43,30 @@ Sends a **professional, rich embed** to your Discord server after each run with:
|
|||||||
|
|
||||||
## 📋 Example Summary
|
## 📋 Example Summary
|
||||||
|
|
||||||
The new webhook format provides a **clean, professional Discord embed** with:
|
|
||||||
|
|
||||||
### Main Summary Card
|
|
||||||
```
|
```
|
||||||
🎯 Microsoft Rewards — Daily Summary
|
🎯 Microsoft Rewards Summary
|
||||||
|
|
||||||
Status: ✅ Success
|
📊 Accounts: 3 • 0 with issues
|
||||||
Version: v2.4.0 • Run ID: abc123xyz
|
💎 Points: 15,230 → 16,890 (+1,660)
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
⏱️ Average Duration: 8m 32s
|
||||||
|
📈 Cumulative Runtime: 25m 36s
|
||||||
|
|
||||||
📊 Global Statistics
|
👤 user1@example.com
|
||||||
💎 Total Points Earned
|
Points: 5,420 → 6,140 (+720)
|
||||||
15,230 → 16,890 (+1,660)
|
Duration: 7m 23s
|
||||||
|
Status: ✅ Completed successfully
|
||||||
|
|
||||||
📊 Accounts Processed
|
👤 user2@example.com
|
||||||
✅ Success: 3 | ⚠️ Errors: 0 | 🚫 Banned: 0
|
Points: 4,810 → 5,750 (+940)
|
||||||
Total: 3 accounts
|
Duration: 9m 41s
|
||||||
|
Status: ✅ Completed successfully
|
||||||
|
|
||||||
⚡ Performance
|
👤 user3@example.com
|
||||||
Average: 553pts/account in 8m 32s
|
Points: 5,000 → 5,000 (+0)
|
||||||
Total Runtime: 25m 36s
|
Duration: 8m 32s
|
||||||
|
Status: ✅ Completed successfully
|
||||||
```
|
```
|
||||||
|
|
||||||
### Per-Account Details
|
|
||||||
```
|
|
||||||
<EFBFBD> Account Details
|
|
||||||
|
|
||||||
✅ user1@example.com
|
|
||||||
└ Points: +720 (🖥️ 450 • 📱 270)
|
|
||||||
└ Duration: 7m 23s
|
|
||||||
|
|
||||||
✅ user2@example.com
|
|
||||||
└ Points: +940 (🖥️ 540 • 📱 400)
|
|
||||||
└ Duration: 9m 41s
|
|
||||||
|
|
||||||
✅ user3@example.com
|
|
||||||
└ Points: +0 (🖥️ 0 • 📱 0)
|
|
||||||
└ Duration: 8m 32s
|
|
||||||
```
|
|
||||||
|
|
||||||
**Color coding:**
|
|
||||||
- 🟢 **Green** — All accounts successful
|
|
||||||
- 🟠 **Orange** — Some accounts with errors
|
|
||||||
- 🔴 **Red** — Banned accounts detected
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Advanced: Separate Channels
|
## 🎯 Advanced: Separate Channels
|
||||||
@@ -104,15 +78,11 @@ Use different webhooks for different notifications:
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://discord.com/api/webhooks/.../errors-channel",
|
"url": "https://discord.com/api/webhooks/.../errors-channel"
|
||||||
"username": "Rewards Errors",
|
|
||||||
"avatarUrl": "https://example.com/error-icon.png"
|
|
||||||
},
|
},
|
||||||
"conclusionWebhook": {
|
"conclusionWebhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://discord.com/api/webhooks/.../summary-channel",
|
"url": "https://discord.com/api/webhooks/.../summary-channel"
|
||||||
"username": "Rewards Summary",
|
|
||||||
"avatarUrl": "https://example.com/success-icon.png"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,53 +91,6 @@ Use different webhooks for different notifications:
|
|||||||
- **`webhook`** — Real-time errors during execution
|
- **`webhook`** — Real-time errors during execution
|
||||||
- **`conclusionWebhook`** — End-of-run summary
|
- **`conclusionWebhook`** — End-of-run summary
|
||||||
|
|
||||||
### 🎨 Customize Webhook Appearance
|
|
||||||
|
|
||||||
You can personalize how your webhook appears in Discord:
|
|
||||||
|
|
||||||
- **`username`** — The display name shown in Discord (default: "Microsoft Rewards")
|
|
||||||
- **`avatarUrl`** — Direct URL to an image for the webhook's avatar
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```jsonc
|
|
||||||
"conclusionWebhook": {
|
|
||||||
"enabled": true,
|
|
||||||
"url": "YOUR_WEBHOOK_URL",
|
|
||||||
"username": "My Custom Bot Name",
|
|
||||||
"avatarUrl": "https://i.imgur.com/YourImage.png"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **💡 Tip:** If you set custom values for both `webhook` and `conclusionWebhook`, the conclusion webhook will use its own settings. Otherwise, it falls back to the main webhook settings.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 New Enhanced Format Features
|
|
||||||
|
|
||||||
The **v2.4+** webhook format includes:
|
|
||||||
|
|
||||||
### Visual Improvements
|
|
||||||
- ✨ **Clean, professional layout** with clear sections
|
|
||||||
- 🎨 **Color-coded status** (Green/Orange/Red based on results)
|
|
||||||
- 📊 **Thumbnail image** for brand recognition
|
|
||||||
- ⏰ **Footer with timestamp** and version info
|
|
||||||
- 🔢 **Formatted numbers** with thousands separators
|
|
||||||
|
|
||||||
### Better Organization
|
|
||||||
- **Main embed** — Global statistics and summary
|
|
||||||
- **Detail fields** — Per-account breakdown (auto-splits for many accounts)
|
|
||||||
- **Smart truncation** — Long emails and errors are shortened intelligently
|
|
||||||
- **Status icons** — ✅ Success, ⚠️ Warning, 🚫 Banned
|
|
||||||
|
|
||||||
### More Information
|
|
||||||
- **Before/After points** — Clear progression tracking
|
|
||||||
- **Desktop vs Mobile** breakdown per account
|
|
||||||
- **Success rate** — Quick glance at account health
|
|
||||||
- **Performance metrics** — Average time and points per account
|
|
||||||
|
|
||||||
### Smart Splitting
|
|
||||||
If you have many accounts (5+), the webhook automatically splits details into multiple fields to avoid hitting Discord's character limits.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Troubleshooting
|
## 🛠️ Troubleshooting
|
||||||
|
|||||||
148
docs/getting-started.md
Normal file
148
docs/getting-started.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 🚀 Getting Started
|
||||||
|
|
||||||
|
**From zero to your first run in 10 minutes**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Requirements
|
||||||
|
|
||||||
|
- **Node.js 20+** → [Download here](https://nodejs.org/)
|
||||||
|
- **Microsoft accounts** with email + password
|
||||||
|
- *Optional:* Docker for containers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Quick Setup (Recommended)
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```powershell
|
||||||
|
setup\setup.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux / macOS
|
||||||
|
```bash
|
||||||
|
bash setup/setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Does It Do?
|
||||||
|
|
||||||
|
1. ✅ Asks for your Microsoft credentials
|
||||||
|
2. ✅ Creates `accounts.json` automatically
|
||||||
|
3. ✅ Installs dependencies
|
||||||
|
4. ✅ Builds the project
|
||||||
|
5. ✅ Runs your first automation (optional)
|
||||||
|
|
||||||
|
**That's it! 🎉**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 After Installation
|
||||||
|
|
||||||
|
### 1️⃣ Enable Scheduler (Recommended)
|
||||||
|
|
||||||
|
Run automatically once per day:
|
||||||
|
|
||||||
|
**Edit** `src/config.jsonc`:
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"schedule": {
|
||||||
|
"enabled": true,
|
||||||
|
"time": "09:00",
|
||||||
|
"timeZone": "America/New_York"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Start scheduler:**
|
||||||
|
```bash
|
||||||
|
npm run start:schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
→ **[Full Scheduler Guide](./schedule.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ Add Notifications (Optional)
|
||||||
|
|
||||||
|
Get a summary after each run:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"conclusionWebhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ **[Discord Setup](./conclusionwebhook.md)** | **[NTFY Setup](./ntfy.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ Enable Humanization (Anti-Ban)
|
||||||
|
|
||||||
|
More natural behavior:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"humanization": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ **[Humanization Guide](./humanization.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Common Issues
|
||||||
|
|
||||||
|
| Problem | Solution |
|
||||||
|
|---------|----------|
|
||||||
|
| **"Node.js not found"** | Install Node.js 20+ and restart terminal |
|
||||||
|
| **"accounts.json missing"** | Run `setup/setup.bat` or create manually |
|
||||||
|
| **"Login failed"** | Check email/password in `accounts.json` |
|
||||||
|
| **"2FA prompt"** | Add TOTP secret → [2FA Guide](./accounts.md) |
|
||||||
|
| **Script crashes** | Check [Diagnostics Guide](./diagnostics.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Manual Setup (Advanced)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Click to expand</strong></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Configure accounts
|
||||||
|
cp src/accounts.example.json src/accounts.json
|
||||||
|
# Edit accounts.json with your credentials
|
||||||
|
|
||||||
|
# 2. Install & build
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 3. Run
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Next Steps
|
||||||
|
|
||||||
|
**Everything works?**
|
||||||
|
→ **[Setup Scheduler](./schedule.md)** for daily automation
|
||||||
|
|
||||||
|
**Need 2FA?**
|
||||||
|
→ **[Accounts & TOTP Guide](./accounts.md)**
|
||||||
|
|
||||||
|
**Want Docker?**
|
||||||
|
→ **[Docker Guide](./docker.md)**
|
||||||
|
|
||||||
|
**Having issues?**
|
||||||
|
→ **[Diagnostics](./diagnostics.md)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**[← Back to Hub](./index.md)** | **[All Docs](./index.md)**
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "2.4.0",
|
"version": "2.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "2.4.0",
|
"version": "2.3.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "2.4.0",
|
"version": "2.3.0",
|
||||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"start:schedule": "node --enable-source-maps ./dist/scheduler.js",
|
"start:schedule": "node --enable-source-maps ./dist/scheduler.js",
|
||||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"setup": "node ./setup/update/setup.mjs",
|
"setup": "node ./setup/setup.mjs",
|
||||||
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
|
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
|
||||||
"create-docker": "docker build -t microsoft-rewards-script-docker ."
|
"create-docker": "docker build -t microsoft-rewards-script-docker ."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal
|
||||||
REM Wrapper to run setup via npm (Windows)
|
REM Lightweight wrapper to run setup.mjs without prereq detection (Windows)
|
||||||
REM Navigates to project root and runs npm run setup
|
REM Assumes Node is already installed and available in PATH.
|
||||||
|
|
||||||
set SCRIPT_DIR=%~dp0
|
set SCRIPT_DIR=%~dp0
|
||||||
set PROJECT_ROOT=%SCRIPT_DIR%..
|
set SETUP_FILE=%SCRIPT_DIR%setup.mjs
|
||||||
|
|
||||||
if not exist "%PROJECT_ROOT%\package.json" (
|
if not exist "%SETUP_FILE%" (
|
||||||
echo [ERROR] package.json not found in project root.
|
echo [ERROR] setup.mjs not found next to this batch file.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Navigating to project root...
|
echo Running setup script...
|
||||||
cd /d "%PROJECT_ROOT%"
|
node "%SETUP_FILE%"
|
||||||
|
|
||||||
echo Running setup script via npm...
|
|
||||||
call npm run setup
|
|
||||||
set EXITCODE=%ERRORLEVEL%
|
set EXITCODE=%ERRORLEVEL%
|
||||||
echo.
|
echo.
|
||||||
echo Setup finished with exit code %EXITCODE%.
|
echo Setup finished with exit code %EXITCODE%.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Unified cross-platform setup script for Microsoft Rewards Script V2.
|
* Unified cross-platform setup script for Microsoft Rewards Script V2.
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Renames accounts.example.jsonc -> accounts.json (idempotent)
|
* - Renames accounts.example.json -> accounts.json (idempotent)
|
||||||
* - Guides user through account configuration (email, password, TOTP, proxy)
|
* - Guides user through account configuration (email, password, TOTP, proxy)
|
||||||
* - Explains config.jsonc structure and key settings
|
* - Explains config.jsonc structure and key settings
|
||||||
* - Installs dependencies (npm install)
|
* - Installs dependencies (npm install)
|
||||||
@@ -25,8 +25,8 @@ import { spawn } from 'child_process';
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
// Project root = two levels up from setup/update directory
|
// Project root = parent of this setup directory
|
||||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
||||||
|
|
||||||
function log(msg) { console.log(msg); }
|
function log(msg) { console.log(msg); }
|
||||||
@@ -35,16 +35,16 @@ function error(msg) { console.error(msg); }
|
|||||||
|
|
||||||
function renameAccountsIfNeeded() {
|
function renameAccountsIfNeeded() {
|
||||||
const accounts = path.join(SRC_DIR, 'accounts.json');
|
const accounts = path.join(SRC_DIR, 'accounts.json');
|
||||||
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
|
const example = path.join(SRC_DIR, 'accounts.example.json');
|
||||||
if (fs.existsSync(accounts)) {
|
if (fs.existsSync(accounts)) {
|
||||||
log('accounts.json already exists - skipping rename.');
|
log('accounts.json already exists - skipping rename.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fs.existsSync(example)) {
|
if (fs.existsSync(example)) {
|
||||||
log('Renaming accounts.example.jsonc to accounts.json...');
|
log('Renaming accounts.example.json to accounts.json...');
|
||||||
fs.renameSync(example, accounts);
|
fs.renameSync(example, accounts);
|
||||||
} else {
|
} else {
|
||||||
warn('Neither accounts.json nor accounts.example.jsonc found.');
|
warn('Neither accounts.json nor accounts.example.json found.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Wrapper to run setup via npm (Linux/macOS)
|
# Wrapper to run unified Node setup script (setup/setup.mjs) regardless of CWD.
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
SETUP_FILE="${SCRIPT_DIR}/setup.mjs"
|
||||||
|
|
||||||
echo "=== Prerequisite Check ==="
|
echo "=== Prerequisite Check ==="
|
||||||
|
|
||||||
if command -v npm >/dev/null 2>&1; then
|
if command -v node >/dev/null 2>&1; then
|
||||||
NPM_VERSION="$(npm -v 2>/dev/null || true)"
|
NODE_VERSION="$(node -v 2>/dev/null || true)"
|
||||||
echo "npm detected: ${NPM_VERSION}"
|
echo "Node detected: ${NODE_VERSION}"
|
||||||
else
|
else
|
||||||
echo "[ERROR] npm not detected."
|
echo "[WARN] Node.js not detected."
|
||||||
echo " Install Node.js and npm from nodejs.org or your package manager"
|
echo " Install (Linux): use your package manager (e.g. 'sudo apt install nodejs npm' or install from nodejs.org for latest)."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v git >/dev/null 2>&1; then
|
if command -v git >/dev/null 2>&1; then
|
||||||
@@ -24,12 +23,19 @@ else
|
|||||||
echo " Install (Linux): e.g. 'sudo apt install git' (or your distro equivalent)."
|
echo " Install (Linux): e.g. 'sudo apt install git' (or your distro equivalent)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "${PROJECT_ROOT}/package.json" ]; then
|
if [ -z "${NODE_VERSION:-}" ]; then
|
||||||
echo "[ERROR] package.json not found at ${PROJECT_ROOT}" >&2
|
read -r -p "Continue anyway? (yes/no) : " CONTINUE
|
||||||
|
case "${CONTINUE,,}" in
|
||||||
|
yes|y) ;;
|
||||||
|
*) echo "Aborting. Install prerequisites then re-run."; exit 1;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${SETUP_FILE}" ]; then
|
||||||
|
echo "[ERROR] setup.mjs not found at ${SETUP_FILE}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== Running setup script via npm ==="
|
echo "=== Running setup script ==="
|
||||||
cd "${PROJECT_ROOT}"
|
exec node "${SETUP_FILE}"
|
||||||
exec npm run setup
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
/* eslint-disable linebreak-style */
|
/* eslint-disable linebreak-style */
|
||||||
/**
|
/**
|
||||||
* Smart Auto-Update Script
|
* Post-run auto-update script
|
||||||
*
|
* - If invoked with --git, runs: git fetch --all --prune; git pull --ff-only; npm ci; npm run build
|
||||||
* Intelligently updates while preserving user settings:
|
* - If invoked with --docker, runs: docker compose pull; docker compose up -d
|
||||||
* - ALWAYS updates code files (*.ts, *.js, etc.)
|
|
||||||
* - ONLY updates config.jsonc if remote has changes to it
|
|
||||||
* - ONLY updates accounts.json if remote has changes to it
|
|
||||||
* - KEEPS user passwords/emails/settings otherwise
|
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node setup/update/update.mjs --git
|
* node setup/update/update.mjs --git
|
||||||
* node setup/update/update.mjs --docker
|
* node setup/update/update.mjs --docker
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Commands are safe-by-default: use --ff-only for pull to avoid merge commits.
|
||||||
|
* - Script is no-op if the relevant tool is not available or commands fail.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, execSync } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
||||||
import { join } from 'node:path'
|
|
||||||
|
|
||||||
function run(cmd, args, opts = {}) {
|
function run(cmd, args, opts = {}) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -27,166 +25,20 @@ function run(cmd, args, opts = {}) {
|
|||||||
|
|
||||||
async function which(cmd) {
|
async function which(cmd) {
|
||||||
const probe = process.platform === 'win32' ? 'where' : 'which'
|
const probe = process.platform === 'win32' ? 'where' : 'which'
|
||||||
const code = await run(probe, [cmd], { stdio: 'ignore' })
|
const code = await run(probe, [cmd])
|
||||||
return code === 0
|
return code === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(cmd) {
|
|
||||||
try {
|
|
||||||
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateGit() {
|
async function updateGit() {
|
||||||
const hasGit = await which('git')
|
const hasGit = await which('git')
|
||||||
if (!hasGit) {
|
if (!hasGit) return 1
|
||||||
console.log('Git not found. Skipping update.')
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(60))
|
|
||||||
console.log('Smart Git Update')
|
|
||||||
console.log('='.repeat(60))
|
|
||||||
|
|
||||||
// Step 1: Read config to get user preferences
|
|
||||||
let userConfig = { autoUpdateConfig: false, autoUpdateAccounts: false }
|
|
||||||
try {
|
|
||||||
if (existsSync('src/config.jsonc')) {
|
|
||||||
const configContent = readFileSync('src/config.jsonc', 'utf8')
|
|
||||||
.replace(/\/\/.*$/gm, '') // remove comments
|
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
|
||||||
const config = JSON.parse(configContent)
|
|
||||||
if (config.update) {
|
|
||||||
userConfig.autoUpdateConfig = config.update.autoUpdateConfig ?? false
|
|
||||||
userConfig.autoUpdateAccounts = config.update.autoUpdateAccounts ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Warning: Could not read config.jsonc, using defaults (preserve local files)')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nUser preferences:')
|
|
||||||
console.log(` Auto-update config.jsonc: ${userConfig.autoUpdateConfig}`)
|
|
||||||
console.log(` Auto-update accounts.json: ${userConfig.autoUpdateAccounts}`)
|
|
||||||
|
|
||||||
// Step 2: Fetch
|
|
||||||
console.log('\nFetching latest changes...')
|
|
||||||
await run('git', ['fetch', '--all', '--prune'])
|
await run('git', ['fetch', '--all', '--prune'])
|
||||||
|
const pullCode = await run('git', ['pull', '--ff-only'])
|
||||||
// Step 3: Get current branch
|
if (pullCode !== 0) return pullCode
|
||||||
const currentBranch = exec('git branch --show-current')
|
|
||||||
if (!currentBranch) {
|
|
||||||
console.log('Could not determine current branch.')
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Check which files changed in remote
|
|
||||||
const remoteBranch = `origin/${currentBranch}`
|
|
||||||
const filesChanged = exec(`git diff --name-only HEAD ${remoteBranch}`)
|
|
||||||
|
|
||||||
if (!filesChanged) {
|
|
||||||
console.log('Already up to date!')
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedFiles = filesChanged.split('\n').filter(f => f.trim())
|
|
||||||
const configChanged = changedFiles.includes('src/config.jsonc')
|
|
||||||
const accountsChanged = changedFiles.includes('src/accounts.json')
|
|
||||||
|
|
||||||
// Step 5: ALWAYS backup config and accounts (smart strategy!)
|
|
||||||
const backupDir = join(process.cwd(), '.update-backup')
|
|
||||||
mkdirSync(backupDir, { recursive: true })
|
|
||||||
|
|
||||||
const filesToRestore = []
|
|
||||||
|
|
||||||
if (existsSync('src/config.jsonc')) {
|
|
||||||
console.log('\nBacking up config.jsonc...')
|
|
||||||
writeFileSync(join(backupDir, 'config.jsonc'), readFileSync('src/config.jsonc', 'utf8'))
|
|
||||||
// Restore if: remote changed it AND user doesn't want auto-update
|
|
||||||
if (configChanged && !userConfig.autoUpdateConfig) {
|
|
||||||
filesToRestore.push('config.jsonc')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync('src/accounts.json')) {
|
|
||||||
console.log('Backing up accounts.json...')
|
|
||||||
writeFileSync(join(backupDir, 'accounts.json'), readFileSync('src/accounts.json', 'utf8'))
|
|
||||||
// Restore if: remote changed it AND user doesn't want auto-update
|
|
||||||
if (accountsChanged && !userConfig.autoUpdateAccounts) {
|
|
||||||
filesToRestore.push('accounts.json')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show what will happen
|
|
||||||
console.log('\nRemote changes:')
|
|
||||||
if (configChanged) {
|
|
||||||
console.log(` config.jsonc: ${userConfig.autoUpdateConfig ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
|
|
||||||
} else {
|
|
||||||
console.log(' config.jsonc: no changes in remote')
|
|
||||||
}
|
|
||||||
if (accountsChanged) {
|
|
||||||
console.log(` accounts.json: ${userConfig.autoUpdateAccounts ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
|
|
||||||
} else {
|
|
||||||
console.log(' accounts.json: no changes in remote')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Stash changes
|
|
||||||
const hasChanges = exec('git status --porcelain')
|
|
||||||
if (hasChanges) {
|
|
||||||
console.log('\nStashing local changes...')
|
|
||||||
await run('git', ['stash', 'push', '-m', 'Auto-update backup'])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 7: Pull
|
|
||||||
console.log('\nPulling latest code...')
|
|
||||||
const pullCode = await run('git', ['pull', '--rebase'])
|
|
||||||
|
|
||||||
if (pullCode !== 0) {
|
|
||||||
console.log('Pull failed.')
|
|
||||||
if (hasChanges) await run('git', ['stash', 'pop'])
|
|
||||||
return pullCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 8: Restore files based on user preferences
|
|
||||||
if (filesToRestore.length > 0) {
|
|
||||||
console.log('\nRestoring local files (per your config preferences)...')
|
|
||||||
for (const file of filesToRestore) {
|
|
||||||
const content = readFileSync(join(backupDir, file), 'utf8')
|
|
||||||
writeFileSync(join('src', file), content)
|
|
||||||
console.log(` ✓ Restored ${file}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 9: Restore stash
|
|
||||||
if (hasChanges) {
|
|
||||||
await run('git', ['stash', 'pop'])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 9: Install & build
|
|
||||||
const hasNpm = await which('npm')
|
const hasNpm = await which('npm')
|
||||||
if (!hasNpm) return 0
|
if (!hasNpm) return 0
|
||||||
|
|
||||||
console.log('\nInstalling dependencies...')
|
|
||||||
await run('npm', ['ci'])
|
await run('npm', ['ci'])
|
||||||
|
return run('npm', ['run', 'build'])
|
||||||
console.log('\nBuilding project...')
|
|
||||||
const buildCode = await run('npm', ['run', 'build'])
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(60))
|
|
||||||
console.log('Update completed!')
|
|
||||||
console.log('='.repeat(60))
|
|
||||||
|
|
||||||
// Optional: Show community info for advanced features
|
|
||||||
if (Math.random() > 0.7) {
|
|
||||||
console.log('\n💡 Tip: Looking for faster updates and advanced features?')
|
|
||||||
console.log(' Check out enhanced builds with better anti-detection.')
|
|
||||||
console.log(' Community: https://discord.gg/kn3695Kx32')
|
|
||||||
}
|
|
||||||
console.log('')
|
|
||||||
|
|
||||||
return buildCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDocker() {
|
async function updateDocker() {
|
||||||
@@ -209,17 +61,7 @@ async function main() {
|
|||||||
if (doDocker && code === 0) {
|
if (doDocker && code === 0) {
|
||||||
code = await updateDocker()
|
code = await updateDocker()
|
||||||
}
|
}
|
||||||
|
process.exit(code)
|
||||||
// Only exit if not called from scheduler
|
|
||||||
// When FROM_SCHEDULER=1, the parent script will handle process lifecycle
|
|
||||||
if (process.env.FROM_SCHEDULER !== '1') {
|
|
||||||
process.exit(code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(() => {
|
main().catch(() => process.exit(1))
|
||||||
// Only exit on error if not called from scheduler
|
|
||||||
if (process.env.FROM_SCHEDULER !== '1') {
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
31
src/accounts.example.json
Normal file
31
src/accounts.example.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"_note": "Microsoft allows up to 3 new accounts per IP per day; in practice it is recommended not to exceed around 5 active accounts per household IP to avoid looking suspicious; there is no official lifetime cap, but creating too many accounts quickly may trigger verification (phone, OTP, captcha); Microsoft discourages bulk account creation from the same IP; unusual activity can result in temporary blocks or account restrictions.",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"email": "email_1",
|
||||||
|
"password": "password_1",
|
||||||
|
"totp": "",
|
||||||
|
"recoveryEmail": "your_email@domain.com",
|
||||||
|
"proxy": {
|
||||||
|
"proxyAxios": true,
|
||||||
|
"url": "",
|
||||||
|
"port": 0,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "email_2",
|
||||||
|
"password": "password_2",
|
||||||
|
"totp": "",
|
||||||
|
"recoveryEmail": "your_email@domain.com",
|
||||||
|
"proxy": {
|
||||||
|
"proxyAxios": true,
|
||||||
|
"url": "",
|
||||||
|
"port": 0,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 📧 MICROSOFT ACCOUNTS CONFIGURATION
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// ⚠️ IMPORTANT SECURITY NOTICE
|
|
||||||
// This file contains sensitive credentials. Never commit the real accounts.jsonc to version control.
|
|
||||||
// The .gitignore is configured to exclude accounts.jsonc but you should verify it's not tracked.
|
|
||||||
|
|
||||||
// 📊 MICROSOFT ACCOUNT LIMITS (Unofficial Guidelines)
|
|
||||||
// - New accounts per IP per day: ~3 (official soft limit)
|
|
||||||
// - Recommended active accounts per household IP: ~5 (to avoid suspicion)
|
|
||||||
// - Creating too many accounts quickly may trigger verification (phone, OTP, captcha)
|
|
||||||
// - Unusual activity can result in temporary blocks or account restrictions
|
|
||||||
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 👤 ACCOUNT 1
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
// Enable or disable this account (true = active, false = skip)
|
|
||||||
"enabled": true,
|
|
||||||
|
|
||||||
// Microsoft account email address
|
|
||||||
"email": "email_1@outlook.com",
|
|
||||||
|
|
||||||
// Account password
|
|
||||||
"password": "password_1",
|
|
||||||
|
|
||||||
// Two-Factor Authentication (2FA) TOTP secret (optional but HIGHLY recommended for security)
|
|
||||||
// Get this from your authenticator app (e.g., Microsoft Authenticator, Google Authenticator)
|
|
||||||
// Format: base32 secret key (e.g., "JBSWY3DPEHPK3PXP")
|
|
||||||
// Leave empty "" if 2FA is not enabled
|
|
||||||
"totp": "",
|
|
||||||
|
|
||||||
// ⚠️ REQUIRED: Recovery email address associated with this Microsoft account
|
|
||||||
// During login, Microsoft shows the first 2 characters and the domain of the recovery email (e.g., "ab***@example.com")
|
|
||||||
// This field is MANDATORY to detect account compromise or bans:
|
|
||||||
// - The script compares what Microsoft displays with this configured recovery email
|
|
||||||
// - If they don't match, it alerts you that the account may be compromised or the recovery email was changed
|
|
||||||
// - This security check helps identify hijacked accounts before they cause issues
|
|
||||||
// Format: Full recovery email address (e.g., "backup@gmail.com")
|
|
||||||
"recoveryEmail": "your_email@domain.com",
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 🌐 PROXY CONFIGURATION (Optional)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
"proxy": {
|
|
||||||
// Enable proxy for HTTP requests (axios/API calls)
|
|
||||||
// If false, proxy is only used for browser automation
|
|
||||||
"proxyAxios": true,
|
|
||||||
|
|
||||||
// Proxy server URL (without protocol)
|
|
||||||
// Examples: "proxy.example.com", "123.45.67.89"
|
|
||||||
// Leave empty "" to disable proxy for this account
|
|
||||||
"url": "",
|
|
||||||
|
|
||||||
// Proxy port number
|
|
||||||
"port": 0,
|
|
||||||
|
|
||||||
// Proxy authentication username (leave empty if no auth required)
|
|
||||||
"username": "",
|
|
||||||
|
|
||||||
// Proxy authentication password (leave empty if no auth required)
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 👤 ACCOUNT 2
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
"enabled": false,
|
|
||||||
"email": "email_2@outlook.com",
|
|
||||||
"password": "password_2",
|
|
||||||
"totp": "",
|
|
||||||
"recoveryEmail": "your_email@domain.com",
|
|
||||||
|
|
||||||
"proxy": {
|
|
||||||
"proxyAxios": true,
|
|
||||||
"url": "",
|
|
||||||
"port": 0,
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 👤 ACCOUNT 3
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
"enabled": false,
|
|
||||||
"email": "email_3@outlook.com",
|
|
||||||
"password": "password_3",
|
|
||||||
"totp": "",
|
|
||||||
"recoveryEmail": "your_email@domain.com",
|
|
||||||
|
|
||||||
"proxy": {
|
|
||||||
"proxyAxios": true,
|
|
||||||
"url": "",
|
|
||||||
"port": 0,
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 👤 ACCOUNT 4
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
"enabled": false,
|
|
||||||
"email": "email_4@outlook.com",
|
|
||||||
"password": "password_4",
|
|
||||||
"totp": "",
|
|
||||||
"recoveryEmail": "your_email@domain.com",
|
|
||||||
|
|
||||||
"proxy": {
|
|
||||||
"proxyAxios": true,
|
|
||||||
"url": "",
|
|
||||||
"port": 0,
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
// ============================================================
|
|
||||||
// 👤 ACCOUNT 5
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
"enabled": false,
|
|
||||||
"email": "email_5@outlook.com",
|
|
||||||
"password": "password_5",
|
|
||||||
"totp": "",
|
|
||||||
"recoveryEmail": "your_email@domain.com",
|
|
||||||
|
|
||||||
"proxy": {
|
|
||||||
"proxyAxios": true,
|
|
||||||
"url": "",
|
|
||||||
"port": 0,
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -54,8 +54,7 @@ class Browser {
|
|||||||
const engineName = 'chromium' // current hard-coded engine
|
const engineName = 'chromium' // current hard-coded engine
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) // explicit engine log
|
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) // explicit engine log
|
||||||
browser = await playwright.chromium.launch({
|
browser = await playwright.chromium.launch({
|
||||||
// Optional: uncomment to use Edge instead of Chromium
|
//channel: 'msedge', // Uses Edge instead of chrome
|
||||||
// channel: 'msedge',
|
|
||||||
headless,
|
headless,
|
||||||
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
||||||
args: [
|
args: [
|
||||||
@@ -71,7 +70,7 @@ class Browser {
|
|||||||
const msg = (e instanceof Error ? e.message : String(e))
|
const msg = (e instanceof Error ? e.message : String(e))
|
||||||
// Common missing browser executable guidance
|
// Common missing browser executable guidance
|
||||||
if (/Executable doesn't exist/i.test(msg)) {
|
if (/Executable doesn't exist/i.test(msg)) {
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run "npm run pre-build" to install all dependencies (or set AUTO_INSTALL_BROWSERS=1 to auto-attempt).', 'error')
|
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run: "npx playwright install chromium" (or set AUTO_INSTALL_BROWSERS=1 to auto attempt).', 'error')
|
||||||
} else {
|
} else {
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
|
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,8 @@
|
|||||||
"runOnZeroPoints": false,
|
"runOnZeroPoints": false,
|
||||||
// Number of account clusters (processes) to run concurrently
|
// Number of account clusters (processes) to run concurrently
|
||||||
"clusters": 1,
|
"clusters": 1,
|
||||||
// How many times to run through all accounts in sequence (1 = process each account once, 2 = twice, etc.)
|
// Number of passes per invocation (usually 1)
|
||||||
// Higher values can catch missed tasks but increase detection risk
|
"passesPerRun": 1
|
||||||
"passesPerRun": 3
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"schedule": {
|
"schedule": {
|
||||||
@@ -208,18 +207,12 @@
|
|||||||
// Live logs webhook (Discord or similar). URL = your webhook endpoint
|
// Live logs webhook (Discord or similar). URL = your webhook endpoint
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"url": "",
|
"url": ""
|
||||||
// Optional: Customize webhook appearance
|
|
||||||
"username": "Live Logs",
|
|
||||||
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
|
|
||||||
},
|
},
|
||||||
// Rich end-of-run summary webhook (Discord or similar)
|
// Rich end-of-run summary webhook (Discord or similar)
|
||||||
"conclusionWebhook": {
|
"conclusionWebhook": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"url": "",
|
"url": ""
|
||||||
// Optional: Customize webhook appearance (overrides webhook settings for conclusion messages)
|
|
||||||
"username": "Microsoft Rewards",
|
|
||||||
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
|
|
||||||
},
|
},
|
||||||
// NTFY push notifications (plain text)
|
// NTFY push notifications (plain text)
|
||||||
"ntfy": {
|
"ntfy": {
|
||||||
@@ -300,44 +293,6 @@
|
|||||||
"git": true,
|
"git": true,
|
||||||
"docker": false,
|
"docker": false,
|
||||||
// Custom updater script path (relative to repo root)
|
// Custom updater script path (relative to repo root)
|
||||||
"scriptPath": "setup/update/update.mjs",
|
"scriptPath": "setup/update/update.mjs"
|
||||||
|
|
||||||
// ⚠️ SMART UPDATE CONTROL - How It Really Works:
|
|
||||||
//
|
|
||||||
// BACKUP: Your files are ALWAYS backed up to .update-backup/ before any update
|
|
||||||
//
|
|
||||||
// UPDATE PROCESS:
|
|
||||||
// 1. Script checks if remote modified config.jsonc or accounts.json
|
|
||||||
// 2. Runs "git pull --rebase" to merge remote changes
|
|
||||||
// 3. Git intelligently merges:
|
|
||||||
// ✅ NEW FIELDS ADDED (new config options, new account properties)
|
|
||||||
// → Your existing values are PRESERVED, new fields are added alongside
|
|
||||||
// → This is 95% of updates - works perfectly without conflicts
|
|
||||||
//
|
|
||||||
// ⚠️ MAJOR RESTRUCTURING (fields renamed, sections reordered, format changed)
|
|
||||||
// → Git may choose one version over the other
|
|
||||||
// → Risk of losing your custom values in restructured sections
|
|
||||||
//
|
|
||||||
// WHAT THE OPTIONS DO:
|
|
||||||
// - true: ACCEPT git merge result (keeps new features + your settings in most cases)
|
|
||||||
// - false: REJECT remote changes, RESTORE your local file from backup (stay on old version)
|
|
||||||
//
|
|
||||||
// RECOMMENDED: Keep both TRUE
|
|
||||||
// Why? Because we rarely restructure files. Most updates just ADD new optional fields.
|
|
||||||
// Your passwords, emails, and custom settings survive addition-only updates.
|
|
||||||
// Only risk: major file restructuring (rare, usually announced in release notes).
|
|
||||||
//
|
|
||||||
// SAFETY NET: Check .update-backup/ folder after updates to compare if worried.
|
|
||||||
|
|
||||||
// Apply remote updates to config.jsonc via git merge
|
|
||||||
// true = accept new features + intelligent merge (RECOMMENDED for most users)
|
|
||||||
// false = always keep your local version (miss new config options)
|
|
||||||
"autoUpdateConfig": true,
|
|
||||||
|
|
||||||
// Apply remote updates to accounts.json via git merge
|
|
||||||
// true = accept new fields (like "region", "totpSecret") while keeping credentials (RECOMMENDED)
|
|
||||||
// false = always keep your local accounts file (safest but may miss new optional fields)
|
|
||||||
"autoUpdateAccounts": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,12 +63,5 @@ export const DISCORD = {
|
|||||||
COLOR_CRIMSON: 0xDC143C,
|
COLOR_CRIMSON: 0xDC143C,
|
||||||
COLOR_ORANGE: 0xFFA500,
|
COLOR_ORANGE: 0xFFA500,
|
||||||
COLOR_BLUE: 0x3498DB,
|
COLOR_BLUE: 0x3498DB,
|
||||||
COLOR_GREEN: 0x00D26A,
|
COLOR_GREEN: 0x00D26A
|
||||||
AVATAR_URL: 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const META = {
|
|
||||||
|
|
||||||
C: 'aHR0cHM6Ly9kaXNjb3JkLmdnL2tuMzY5NUt4MzI=',
|
|
||||||
R: 'aHR0cHM6Ly9naXRodWIuY29tL0xpZ2h0NjAtMS9NaWNyb3NvZnQtUmV3YXJkcy1SZXdp'
|
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ const SELECTORS = {
|
|||||||
const LOGIN_TARGET = { host: 'rewards.bing.com', path: '/' }
|
const LOGIN_TARGET = { host: 'rewards.bing.com', path: '/' }
|
||||||
|
|
||||||
const DEFAULT_TIMEOUTS = {
|
const DEFAULT_TIMEOUTS = {
|
||||||
loginMaxMs: (() => {
|
loginMaxMs: Number(process.env.LOGIN_MAX_WAIT_MS || 180000), // 3 min
|
||||||
const val = Number(process.env.LOGIN_MAX_WAIT_MS || 180000)
|
|
||||||
if (isNaN(val) || val < 10000 || val > 600000) {
|
|
||||||
console.warn(`[Login] Invalid LOGIN_MAX_WAIT_MS: ${process.env.LOGIN_MAX_WAIT_MS}. Using default 180000ms`)
|
|
||||||
return 180000
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
})(),
|
|
||||||
short: 500,
|
short: 500,
|
||||||
medium: 1500,
|
medium: 1500,
|
||||||
long: 3000
|
long: 3000
|
||||||
@@ -78,12 +71,6 @@ export class Login {
|
|||||||
// --------------- Public API ---------------
|
// --------------- Public API ---------------
|
||||||
async login(page: Page, email: string, password: string, totpSecret?: string) {
|
async login(page: Page, email: string, password: string, totpSecret?: string) {
|
||||||
try {
|
try {
|
||||||
// Clear any existing intervals from previous runs
|
|
||||||
if (this.compromisedInterval) {
|
|
||||||
clearInterval(this.compromisedInterval)
|
|
||||||
this.compromisedInterval = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
||||||
this.currentTotpSecret = (totpSecret && totpSecret.trim()) || undefined
|
this.currentTotpSecret = (totpSecret && totpSecret.trim()) || undefined
|
||||||
|
|
||||||
@@ -298,45 +285,40 @@ export class Login {
|
|||||||
let userInput: string | null = null
|
let userInput: string | null = null
|
||||||
let checkInterval: NodeJS.Timeout | null = null
|
let checkInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
try {
|
const inputPromise = new Promise<string>(res => {
|
||||||
const inputPromise = new Promise<string>(res => {
|
rl.question('Enter 2FA code:\n', ans => {
|
||||||
rl.question('Enter 2FA code:\n', ans => {
|
if (checkInterval) clearInterval(checkInterval)
|
||||||
|
rl.close()
|
||||||
|
res(ans.trim())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check every 2 seconds if user manually progressed past the dialog
|
||||||
|
checkInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||||
|
// Check if we're no longer on 2FA page
|
||||||
|
const still2FA = await page.locator('input[name="otc"]').first().isVisible({ timeout: 500 }).catch(() => false)
|
||||||
|
if (!still2FA) {
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page changed during 2FA wait (user may have clicked Next)', 'warn')
|
||||||
if (checkInterval) clearInterval(checkInterval)
|
if (checkInterval) clearInterval(checkInterval)
|
||||||
rl.close()
|
rl.close()
|
||||||
res(ans.trim())
|
userInput = 'skip' // Signal to skip submission
|
||||||
})
|
}
|
||||||
})
|
} catch {/* ignore */}
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
// Check every 2 seconds if user manually progressed past the dialog
|
const code = await inputPromise
|
||||||
checkInterval = setInterval(async () => {
|
if (checkInterval) clearInterval(checkInterval)
|
||||||
try {
|
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
if (code === 'skip' || userInput === 'skip') {
|
||||||
// Check if we're no longer on 2FA page
|
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipping 2FA code submission (page progressed)')
|
||||||
const still2FA = await page.locator('input[name="otc"]').first().isVisible({ timeout: 500 }).catch(() => false)
|
return
|
||||||
if (!still2FA) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page changed during 2FA wait (user may have clicked Next)', 'warn')
|
|
||||||
if (checkInterval) clearInterval(checkInterval)
|
|
||||||
rl.close()
|
|
||||||
userInput = 'skip' // Signal to skip submission
|
|
||||||
}
|
|
||||||
} catch {/* ignore */}
|
|
||||||
}, 2000)
|
|
||||||
|
|
||||||
const code = await inputPromise
|
|
||||||
|
|
||||||
if (code === 'skip' || userInput === 'skip') {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipping 2FA code submission (page progressed)')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.fill('input[name="otc"]', code)
|
|
||||||
await page.keyboard.press('Enter')
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code submitted')
|
|
||||||
} finally {
|
|
||||||
// Ensure cleanup happens even if errors occur
|
|
||||||
if (checkInterval) clearInterval(checkInterval)
|
|
||||||
try { rl.close() } catch {/* ignore */}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.fill('input[name="otc"]', code)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code submitted')
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureTotpInput(page: Page): Promise<string | null> {
|
private async ensureTotpInput(page: Page): Promise<string | null> {
|
||||||
@@ -776,19 +758,12 @@ export class Login {
|
|||||||
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level)
|
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level)
|
||||||
try {
|
try {
|
||||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||||
const fields = [
|
await ConclusionWebhook(this.bot.config,'', { embeds:[{ title:`🔐 ${incident.kind}`, description:'Security check by @Light', color: severity==='critical'?0xFF0000:0xFFAA00, fields:[
|
||||||
{ name: 'Account', value: incident.account },
|
{ name:'Account', value: incident.account },
|
||||||
...(incident.details?.length ? [{ name: 'Details', value: incident.details.join('\n') }] : []),
|
...(incident.details?.length?[{ name:'Details', value: incident.details.join('\n') }]:[]),
|
||||||
...(incident.next?.length ? [{ name: 'Next steps', value: incident.next.join('\n') }] : []),
|
...(incident.next?.length?[{ name:'Next steps', value: incident.next.join('\n') }]:[]),
|
||||||
...(incident.docsUrl ? [{ name: 'Docs', value: incident.docsUrl }] : [])
|
...(incident.docsUrl?[{ name:'Docs', value: incident.docsUrl }]:[])
|
||||||
]
|
] }] })
|
||||||
await ConclusionWebhook(
|
|
||||||
this.bot.config,
|
|
||||||
`🔐 ${incident.kind}`,
|
|
||||||
'_Security check by @Light_',
|
|
||||||
fields,
|
|
||||||
severity === 'critical' ? 0xFF0000 : 0xFFAA00
|
|
||||||
)
|
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
382
src/index.ts
382
src/index.ts
@@ -43,9 +43,6 @@ export class MicrosoftRewardsBot {
|
|||||||
public compromisedModeActive: boolean = false
|
public compromisedModeActive: boolean = false
|
||||||
public compromisedReason?: string
|
public compromisedReason?: string
|
||||||
public compromisedEmail?: string
|
public compromisedEmail?: string
|
||||||
// Mutex-like flag to prevent parallel execution when config.parallel is accidentally misconfigured
|
|
||||||
private isDesktopRunning: boolean = false
|
|
||||||
private isMobileRunning: boolean = false
|
|
||||||
|
|
||||||
private pointsCanCollect: number = 0
|
private pointsCanCollect: number = 0
|
||||||
private pointsInitial: number = 0
|
private pointsInitial: number = 0
|
||||||
@@ -188,13 +185,24 @@ export class MicrosoftRewardsBot {
|
|||||||
const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => {
|
const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => {
|
||||||
try {
|
try {
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
await ConclusionWebhook(
|
const title = '💳 Spend detected (Buy Mode)'
|
||||||
this.config,
|
const desc = [
|
||||||
'💳 Spend Detected',
|
`Account: ${account.email}`,
|
||||||
`**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`,
|
`Spent: -${delta} points`,
|
||||||
undefined,
|
`Current: ${nowPts} points`,
|
||||||
0xFFAA00
|
`Session spent: ${cumulativeSpent} points`
|
||||||
)
|
].join('\n')
|
||||||
|
await ConclusionWebhook(this.config, '', {
|
||||||
|
context: 'spend',
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
description: desc,
|
||||||
|
// Use warn color so NTFY is sent as warn
|
||||||
|
color: 0xFFAA00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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')
|
||||||
}
|
}
|
||||||
@@ -253,11 +261,7 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save cookies and close monitor; keep main page open for user until they close it themselves
|
// Save cookies and close monitor; keep main page open for user until they close it themselves
|
||||||
try {
|
try { await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile) } catch { /* ignore */ }
|
||||||
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 {/* ignore */}
|
try { if (!monitor.isClosed()) await monitor.close() } catch {/* ignore */}
|
||||||
|
|
||||||
// Send a final minimal conclusion webhook for this manual session
|
// Send a final minimal conclusion webhook for this manual session
|
||||||
@@ -314,23 +318,19 @@ export class MicrosoftRewardsBot {
|
|||||||
╚══════════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════╝
|
||||||
`
|
`
|
||||||
|
|
||||||
// Read package version and build banner info
|
|
||||||
const pkgPath = path.join(__dirname, '../', 'package.json')
|
|
||||||
let version = 'unknown'
|
|
||||||
try {
|
try {
|
||||||
|
const pkgPath = path.join(__dirname, '../', 'package.json')
|
||||||
|
let version = 'unknown'
|
||||||
if (fs.existsSync(pkgPath)) {
|
if (fs.existsSync(pkgPath)) {
|
||||||
const raw = fs.readFileSync(pkgPath, 'utf-8')
|
const raw = fs.readFileSync(pkgPath, 'utf-8')
|
||||||
const pkg = JSON.parse(raw)
|
const pkg = JSON.parse(raw)
|
||||||
version = pkg.version || version
|
version = pkg.version || version
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Ignore version read errors
|
// Show appropriate banner based on mode
|
||||||
}
|
const displayBanner = this.buyMode.enabled ? buyModeBanner : banner
|
||||||
|
console.log(displayBanner)
|
||||||
// Display appropriate banner based on mode
|
console.log('='.repeat(80))
|
||||||
const displayBanner = this.buyMode.enabled ? buyModeBanner : banner
|
|
||||||
console.log(displayBanner)
|
|
||||||
console.log('='.repeat(80))
|
|
||||||
|
|
||||||
if (this.buyMode.enabled) {
|
if (this.buyMode.enabled) {
|
||||||
console.log(` Version: ${version} | Process: ${process.pid} | Buy Mode: Active`)
|
console.log(` Version: ${version} | Process: ${process.pid} | Buy Mode: Active`)
|
||||||
@@ -376,9 +376,19 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('='.repeat(80) + '\n')
|
console.log('='.repeat(80) + '\n')
|
||||||
}
|
} catch {
|
||||||
|
const displayBanner = this.buyMode.enabled ? buyModeBanner : banner
|
||||||
// Return summaries (used when clusters==1)
|
console.log(displayBanner)
|
||||||
|
console.log('='.repeat(50))
|
||||||
|
if (this.buyMode.enabled) {
|
||||||
|
console.log(' Microsoft Rewards Buy Mode Started')
|
||||||
|
console.log(' See buy-mode.md for details')
|
||||||
|
} else {
|
||||||
|
console.log(' Microsoft Rewards Script Started')
|
||||||
|
}
|
||||||
|
console.log('='.repeat(50) + '\n')
|
||||||
|
}
|
||||||
|
} // Return summaries (used when clusters==1)
|
||||||
public getSummaries() {
|
public getSummaries() {
|
||||||
return this.accountSummaries
|
return this.accountSummaries
|
||||||
}
|
}
|
||||||
@@ -387,15 +397,8 @@ export class MicrosoftRewardsBot {
|
|||||||
log('main', 'MAIN-PRIMARY', 'Primary process started')
|
log('main', 'MAIN-PRIMARY', 'Primary process started')
|
||||||
|
|
||||||
const totalAccounts = this.accounts.length
|
const totalAccounts = this.accounts.length
|
||||||
|
|
||||||
// Validate accounts exist
|
|
||||||
if (totalAccounts === 0) {
|
|
||||||
log('main', 'MAIN-PRIMARY', 'No accounts found to process. Exiting.', 'warn')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers.
|
// If user over-specified clusters (e.g. 10 clusters but only 2 accounts), don't spawn useless idle workers.
|
||||||
const workerCount = Math.min(this.config.clusters, totalAccounts)
|
const workerCount = Math.min(this.config.clusters, totalAccounts || 1)
|
||||||
const accountChunks = this.utils.chunkArray(this.accounts, workerCount)
|
const accountChunks = this.utils.chunkArray(this.accounts, workerCount)
|
||||||
// Reset activeWorkers to actual spawn count (constructor used raw clusters)
|
// Reset activeWorkers to actual spawn count (constructor used raw clusters)
|
||||||
this.activeWorkers = workerCount
|
this.activeWorkers = workerCount
|
||||||
@@ -403,13 +406,7 @@ export class MicrosoftRewardsBot {
|
|||||||
for (let i = 0; i < workerCount; i++) {
|
for (let i = 0; i < workerCount; i++) {
|
||||||
const worker = cluster.fork()
|
const worker = cluster.fork()
|
||||||
const chunk = accountChunks[i] || []
|
const chunk = accountChunks[i] || []
|
||||||
|
;(worker as unknown as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk })
|
||||||
// Validate chunk has accounts
|
|
||||||
if (chunk.length === 0) {
|
|
||||||
log('main', 'MAIN-PRIMARY', `Warning: Worker ${i} received empty account chunk`, 'warn')
|
|
||||||
}
|
|
||||||
|
|
||||||
(worker as unknown as { send?: (m: { chunk: Account[] }) => void }).send?.({ chunk })
|
|
||||||
worker.on('message', (msg: unknown) => {
|
worker.on('message', (msg: unknown) => {
|
||||||
const m = msg as { type?: string; data?: AccountSummary[] }
|
const m = msg as { type?: string; data?: AccountSummary[] }
|
||||||
if (m && m.type === 'summary' && Array.isArray(m.data)) {
|
if (m && m.type === 'summary' && Array.isArray(m.data)) {
|
||||||
@@ -451,13 +448,8 @@ export class MicrosoftRewardsBot {
|
|||||||
try {
|
try {
|
||||||
await this.runAutoUpdate()
|
await this.runAutoUpdate()
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
// Only exit if not spawned by scheduler
|
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
||||||
if (!process.env.SCHEDULER_HEARTBEAT_FILE) {
|
process.exit(0)
|
||||||
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
|
||||||
process.exit(0)
|
|
||||||
} else {
|
|
||||||
log('main', 'MAIN-WORKER', 'All workers destroyed. Scheduler mode: returning control to scheduler.')
|
|
||||||
}
|
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -544,72 +536,52 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
errors.push(formatFullErr('mobile', e)); return null
|
errors.push(formatFullErr('mobile', e)); return null
|
||||||
})
|
})
|
||||||
const [desktopResult, mobileResult] = await Promise.allSettled([desktopPromise, mobilePromise])
|
const [desktopResult, mobileResult] = await Promise.all([desktopPromise, mobilePromise])
|
||||||
|
if (desktopResult) {
|
||||||
// Handle desktop result
|
desktopInitial = desktopResult.initialPoints
|
||||||
if (desktopResult.status === 'fulfilled' && desktopResult.value) {
|
desktopCollected = desktopResult.collectedPoints
|
||||||
desktopInitial = desktopResult.value.initialPoints
|
|
||||||
desktopCollected = desktopResult.value.collectedPoints
|
|
||||||
} else if (desktopResult.status === 'rejected') {
|
|
||||||
log(false, 'TASK', `Desktop promise rejected unexpectedly: ${shortErr(desktopResult.reason)}`,'error')
|
|
||||||
errors.push(formatFullErr('desktop-rejected', desktopResult.reason))
|
|
||||||
}
|
}
|
||||||
|
if (mobileResult) {
|
||||||
// Handle mobile result
|
mobileInitial = mobileResult.initialPoints
|
||||||
if (mobileResult.status === 'fulfilled' && mobileResult.value) {
|
mobileCollected = mobileResult.collectedPoints
|
||||||
mobileInitial = mobileResult.value.initialPoints
|
|
||||||
mobileCollected = mobileResult.value.collectedPoints
|
|
||||||
} else if (mobileResult.status === 'rejected') {
|
|
||||||
log(true, 'TASK', `Mobile promise rejected unexpectedly: ${shortErr(mobileResult.reason)}`,'error')
|
|
||||||
errors.push(formatFullErr('mobile-rejected', mobileResult.reason))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sequential execution with safety checks
|
this.isMobile = false
|
||||||
if (this.isDesktopRunning || this.isMobileRunning) {
|
const desktopResult = await this.Desktop(account).catch(e => {
|
||||||
log('main', 'TASK', `Race condition detected: Desktop=${this.isDesktopRunning}, Mobile=${this.isMobileRunning}. Skipping to prevent conflicts.`, 'error')
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
errors.push('race-condition-detected')
|
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
} else {
|
const bd = detectBanReason(e)
|
||||||
this.isMobile = false
|
if (bd.status) {
|
||||||
this.isDesktopRunning = true
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
const desktopResult = await this.Desktop(account).catch(e => {
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
|
}
|
||||||
|
errors.push(formatFullErr('desktop', e)); return null
|
||||||
|
})
|
||||||
|
if (desktopResult) {
|
||||||
|
desktopInitial = desktopResult.initialPoints
|
||||||
|
desktopCollected = desktopResult.collectedPoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// If banned or compromised detected, skip mobile to save time
|
||||||
|
if (!banned.status && !this.compromisedModeActive) {
|
||||||
|
this.isMobile = true
|
||||||
|
const mobileResult = await this.Mobile(account).catch(e => {
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${msg}`,'error')
|
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
||||||
const bd = detectBanReason(e)
|
const bd = detectBanReason(e)
|
||||||
if (bd.status) {
|
if (bd.status) {
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
void this.handleImmediateBanAlert(account.email, banned.reason)
|
||||||
}
|
}
|
||||||
errors.push(formatFullErr('desktop', e)); return null
|
errors.push(formatFullErr('mobile', e)); return null
|
||||||
})
|
})
|
||||||
if (desktopResult) {
|
if (mobileResult) {
|
||||||
desktopInitial = desktopResult.initialPoints
|
mobileInitial = mobileResult.initialPoints
|
||||||
desktopCollected = desktopResult.collectedPoints
|
mobileCollected = mobileResult.collectedPoints
|
||||||
}
|
|
||||||
this.isDesktopRunning = false
|
|
||||||
|
|
||||||
// If banned or compromised detected, skip mobile to save time
|
|
||||||
if (!banned.status && !this.compromisedModeActive) {
|
|
||||||
this.isMobile = true
|
|
||||||
this.isMobileRunning = true
|
|
||||||
const mobileResult = await this.Mobile(account).catch(e => {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e)
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${msg}`,'error')
|
|
||||||
const bd = detectBanReason(e)
|
|
||||||
if (bd.status) {
|
|
||||||
banned.status = true; banned.reason = bd.reason.substring(0,200)
|
|
||||||
void this.handleImmediateBanAlert(account.email, banned.reason)
|
|
||||||
}
|
|
||||||
errors.push(formatFullErr('mobile', e)); return null
|
|
||||||
})
|
|
||||||
if (mobileResult) {
|
|
||||||
mobileInitial = mobileResult.initialPoints
|
|
||||||
mobileCollected = mobileResult.collectedPoints
|
|
||||||
}
|
|
||||||
this.isMobileRunning = false
|
|
||||||
} else {
|
|
||||||
const why = banned.status ? 'banned status' : 'compromised status'
|
|
||||||
log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn')
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const why = banned.status ? 'banned status' : 'compromised status'
|
||||||
|
log(true, 'TASK', `Skipping mobile flow for ${account.email} due to ${why}`, 'warn')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,14 +633,10 @@ export class MicrosoftRewardsBot {
|
|||||||
// If any account is flagged compromised, do NOT exit; keep the process alive so the browser stays open
|
// If any account is flagged compromised, do NOT exit; keep the process alive so the browser stays open
|
||||||
if (this.compromisedModeActive || this.globalStandby.active) {
|
if (this.compromisedModeActive || this.globalStandby.active) {
|
||||||
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done. Security check by @Light','warn','yellow')
|
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done. Security check by @Light','warn','yellow')
|
||||||
// Periodic heartbeat with cleanup on exit
|
// Periodic heartbeat
|
||||||
const standbyInterval = setInterval(() => {
|
setInterval(() => {
|
||||||
log('main','SECURITY','Still in standby: session(s) held open for manual recovery / review...','warn','yellow')
|
log('main','SECURITY','Still in standby: session(s) held open for manual recovery / review...','warn','yellow')
|
||||||
}, 5 * 60 * 1000)
|
}, 5 * 60 * 1000)
|
||||||
|
|
||||||
// Cleanup on process exit
|
|
||||||
process.once('SIGINT', () => { clearInterval(standbyInterval); process.exit(0) })
|
|
||||||
process.once('SIGTERM', () => { clearInterval(standbyInterval); process.exit(0) })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If in worker mode (clusters>1) send summaries to primary
|
// If in worker mode (clusters>1) send summaries to primary
|
||||||
@@ -682,8 +650,10 @@ export class MicrosoftRewardsBot {
|
|||||||
// Cleanup heartbeat timer/file at end of run
|
// Cleanup heartbeat timer/file at end of run
|
||||||
if (this.heartbeatTimer) { try { clearInterval(this.heartbeatTimer) } catch { /* ignore */ } }
|
if (this.heartbeatTimer) { try { clearInterval(this.heartbeatTimer) } catch { /* ignore */ } }
|
||||||
if (this.heartbeatFile) { try { if (fs.existsSync(this.heartbeatFile)) fs.unlinkSync(this.heartbeatFile) } catch { /* ignore */ } }
|
if (this.heartbeatFile) { try { if (fs.existsSync(this.heartbeatFile)) fs.unlinkSync(this.heartbeatFile) } catch { /* ignore */ } }
|
||||||
// After conclusion, run optional auto-update
|
// After conclusion, run optional auto-update (only if not in scheduler mode)
|
||||||
await this.runAutoUpdate().catch(() => {/* ignore update errors */})
|
if (!process.env.SCHEDULER_HEARTBEAT_FILE) {
|
||||||
|
await this.runAutoUpdate().catch(() => {/* ignore update errors */})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Only exit if not spawned by scheduler
|
// Only exit if not spawned by scheduler
|
||||||
if (!process.env.SCHEDULER_HEARTBEAT_FILE) {
|
if (!process.env.SCHEDULER_HEARTBEAT_FILE) {
|
||||||
@@ -697,13 +667,17 @@ export class MicrosoftRewardsBot {
|
|||||||
const h = this.config?.humanization
|
const h = this.config?.humanization
|
||||||
if (!h || h.immediateBanAlert === false) return
|
if (!h || h.immediateBanAlert === false) return
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
await ConclusionWebhook(
|
const title = '🚫 Ban detected'
|
||||||
this.config,
|
const desc = [`Account: ${email}`, `Reason: ${reason || 'detected by heuristics'}`].join('\n')
|
||||||
'🚫 Ban Detected',
|
await ConclusionWebhook(this.config, `${title}\n${desc}`, {
|
||||||
`**Account:** ${email}\n**Reason:** ${reason || 'detected by heuristics'}`,
|
embeds: [
|
||||||
undefined,
|
{
|
||||||
DISCORD.COLOR_RED
|
title,
|
||||||
)
|
description: desc,
|
||||||
|
color: DISCORD.COLOR_RED
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('main','ALERT',`Failed to send ban alert: ${e instanceof Error ? e.message : e}`,'warn')
|
log('main','ALERT',`Failed to send ban alert: ${e instanceof Error ? e.message : e}`,'warn')
|
||||||
}
|
}
|
||||||
@@ -762,20 +736,19 @@ export class MicrosoftRewardsBot {
|
|||||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
||||||
try {
|
try {
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
await ConclusionWebhook(
|
await ConclusionWebhook(this.config, `Security issue on ${account.email} (${reason}). Logged in successfully; leaving browser open. Security check by @Light`, {
|
||||||
this.config,
|
context: 'compromised',
|
||||||
'🔐 Security Alert (Post-Login)',
|
embeds: [
|
||||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving browser open; skipping tasks\n\n_Security check by @Light_`,
|
{
|
||||||
undefined,
|
title: '🔐 Security alert (post-login)',
|
||||||
0xFFAA00
|
description: `Account: ${account.email}\nReason: ${reason}\nAction: Leaving browser open; skipping tasks`,
|
||||||
)
|
color: 0xFFAA00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
// Save session for convenience, but do not close the browser
|
// Save session for convenience, but do not close the browser
|
||||||
try {
|
try { await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) } catch { /* ignore */ }
|
||||||
await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile)
|
|
||||||
} catch (e) {
|
|
||||||
log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
|
||||||
}
|
|
||||||
return { initialPoints: 0, collectedPoints: 0 }
|
return { initialPoints: 0, collectedPoints: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,19 +839,18 @@ export class MicrosoftRewardsBot {
|
|||||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
||||||
try {
|
try {
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
await ConclusionWebhook(
|
await ConclusionWebhook(this.config, `Security issue on ${account.email} (${reason}). Mobile flow halted; leaving browser open. Security check by @Light`, {
|
||||||
this.config,
|
context: 'compromised',
|
||||||
'🔐 Security Alert (Mobile)',
|
embeds: [
|
||||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving mobile browser open; skipping tasks\n\n_Security check by @Light_`,
|
{
|
||||||
undefined,
|
title: '🔐 Security alert (mobile)',
|
||||||
0xFFAA00
|
description: `Account: ${account.email}\nReason: ${reason}\nAction: Leaving mobile browser open; skipping tasks`,
|
||||||
)
|
color: 0xFFAA00
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
} catch {/* ignore */}
|
} catch {/* ignore */}
|
||||||
try {
|
try { await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile) } catch { /* ignore */ }
|
||||||
await saveSessionData(this.config.sessionPath, this.homePage.context(), account.email, this.isMobile)
|
|
||||||
} catch (e) {
|
|
||||||
log(this.isMobile, 'SECURITY', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
|
||||||
}
|
|
||||||
return { initialPoints: 0, collectedPoints: 0 }
|
return { initialPoints: 0, collectedPoints: 0 }
|
||||||
}
|
}
|
||||||
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
|
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
|
||||||
@@ -972,7 +944,7 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendConclusion(summaries: AccountSummary[]) {
|
private async sendConclusion(summaries: AccountSummary[]) {
|
||||||
const { ConclusionWebhookEnhanced } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
const cfg = this.config
|
const cfg = this.config
|
||||||
|
|
||||||
const conclusionWebhookEnabled = !!(cfg.conclusionWebhook && cfg.conclusionWebhook.enabled)
|
const conclusionWebhookEnabled = !!(cfg.conclusionWebhook && cfg.conclusionWebhook.enabled)
|
||||||
@@ -987,7 +959,6 @@ export class MicrosoftRewardsBot {
|
|||||||
let totalEnd = 0
|
let totalEnd = 0
|
||||||
let totalDuration = 0
|
let totalDuration = 0
|
||||||
let accountsWithErrors = 0
|
let accountsWithErrors = 0
|
||||||
let accountsBanned = 0
|
|
||||||
let successes = 0
|
let successes = 0
|
||||||
|
|
||||||
// Calculate summary statistics
|
// Calculate summary statistics
|
||||||
@@ -996,9 +967,8 @@ export class MicrosoftRewardsBot {
|
|||||||
totalInitial += s.initialTotal
|
totalInitial += s.initialTotal
|
||||||
totalEnd += s.endTotal
|
totalEnd += s.endTotal
|
||||||
totalDuration += s.durationMs
|
totalDuration += s.durationMs
|
||||||
if (s.banned?.status) accountsBanned++
|
|
||||||
if (s.errors.length) accountsWithErrors++
|
if (s.errors.length) accountsWithErrors++
|
||||||
if (!s.banned?.status && !s.errors.length) successes++
|
else successes++
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgDuration = totalDuration / totalAccounts
|
const avgDuration = totalDuration / totalAccounts
|
||||||
@@ -1015,23 +985,67 @@ export class MicrosoftRewardsBot {
|
|||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
// Send enhanced webhook
|
// Build clean embed with account details
|
||||||
if (conclusionWebhookEnabled || ntfyEnabled || webhookEnabled) {
|
type DiscordField = { name: string; value: string; inline?: boolean }
|
||||||
await ConclusionWebhookEnhanced(cfg, {
|
type DiscordEmbed = {
|
||||||
version,
|
title?: string
|
||||||
runId: this.runId,
|
description?: string
|
||||||
totalAccounts,
|
color?: number
|
||||||
successes,
|
fields?: DiscordField[]
|
||||||
accountsWithErrors,
|
thumbnail?: { url: string }
|
||||||
accountsBanned,
|
timestamp?: string
|
||||||
totalCollected,
|
footer?: { text: string; icon_url?: string }
|
||||||
totalInitial,
|
}
|
||||||
totalEnd,
|
|
||||||
avgPointsPerAccount,
|
const accountDetails: string[] = []
|
||||||
totalDuration,
|
for (const s of summaries) {
|
||||||
avgDuration,
|
const statusIcon = s.banned?.status ? '🚫' : (s.errors.length ? '⚠️' : '✅')
|
||||||
summaries
|
const line = `${statusIcon} **${s.email}** → +${s.totalCollected}pts (🖥️${s.desktopCollected} 📱${s.mobileCollected}) • ${formatDuration(s.durationMs)}`
|
||||||
|
accountDetails.push(line)
|
||||||
|
if (s.banned?.status) accountDetails.push(` └ Banned: ${s.banned.reason || 'detected'}`)
|
||||||
|
if (s.errors.length > 0) accountDetails.push(` └ Errors: ${s.errors.slice(0, 2).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed: DiscordEmbed = {
|
||||||
|
title: '🎯 Microsoft Rewards - Daily Summary',
|
||||||
|
description: [
|
||||||
|
'**📊 Global Statistics**',
|
||||||
|
`├ Total Points: **${totalInitial}** → **${totalEnd}** (+**${totalCollected}**)`,
|
||||||
|
`├ Accounts: ✅ ${successes} • ${accountsWithErrors > 0 ? `⚠️ ${accountsWithErrors}` : ''} (${totalAccounts} total)`,
|
||||||
|
`├ Average: **${avgPointsPerAccount}pts/account** • **${formatDuration(avgDuration)}/account**`,
|
||||||
|
`└ Runtime: **${formatDuration(totalDuration)}**`,
|
||||||
|
'',
|
||||||
|
'**📈 Account Details**',
|
||||||
|
...accountDetails
|
||||||
|
].filter(Boolean).join('\n'),
|
||||||
|
color: accountsWithErrors > 0 ? DISCORD.COLOR_ORANGE : DISCORD.COLOR_GREEN,
|
||||||
|
thumbnail: {
|
||||||
|
url: 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
footer: {
|
||||||
|
text: `MS Rewards Bot v${version} • Run ${this.runId}`,
|
||||||
|
icon_url: 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NTFY plain text fallback
|
||||||
|
const fallback = [
|
||||||
|
'🎯 Microsoft Rewards Summary',
|
||||||
|
`Accounts: ${totalAccounts} (✅${successes} ${accountsWithErrors > 0 ? `⚠️${accountsWithErrors}` : ''})`,
|
||||||
|
`Total: ${totalInitial}→${totalEnd} (+${totalCollected})`,
|
||||||
|
`Average: ${avgPointsPerAccount}pts/account • ${formatDuration(avgDuration)}`,
|
||||||
|
`Runtime: ${formatDuration(totalDuration)}`,
|
||||||
|
'',
|
||||||
|
...summaries.map(s => {
|
||||||
|
const st = s.banned?.status ? '🚫' : (s.errors.length ? '⚠️' : '✅')
|
||||||
|
return `${st} ${s.email}: +${s.totalCollected}pts (🖥️${s.desktopCollected} 📱${s.mobileCollected})`
|
||||||
})
|
})
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
// Send webhook
|
||||||
|
if (conclusionWebhookEnabled || ntfyEnabled || webhookEnabled) {
|
||||||
|
await ConclusionWebhook(cfg, fallback, { embeds: [embed] })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write local JSON report
|
// Write local JSON report
|
||||||
@@ -1065,11 +1079,6 @@ export class MicrosoftRewardsBot {
|
|||||||
log('main','REPORT',`Failed diagnostics cleanup: ${e instanceof Error ? e.message : e}`,'warn')
|
log('main','REPORT',`Failed diagnostics cleanup: ${e instanceof Error ? e.message : e}`,'warn')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional community notice (shown randomly in ~15% of successful runs)
|
|
||||||
if (Math.random() > 0.85 && successes > 0 && accountsWithErrors === 0) {
|
|
||||||
log('main','INFO','Want faster updates & enhanced anti-detection? Community builds available: https://discord.gg/kn3695Kx32')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reserve one diagnostics slot for this run (caps captures). */
|
/** Reserve one diagnostics slot for this run (caps captures). */
|
||||||
@@ -1116,14 +1125,8 @@ export class MicrosoftRewardsBot {
|
|||||||
if (upd.docker) args.push('--docker')
|
if (upd.docker) args.push('--docker')
|
||||||
if (args.length === 0) return
|
if (args.length === 0) return
|
||||||
|
|
||||||
// Pass scheduler flag to update script so it doesn't exit
|
|
||||||
const isSchedulerMode = !!process.env.SCHEDULER_HEARTBEAT_FILE
|
|
||||||
const env = isSchedulerMode
|
|
||||||
? { ...process.env, FROM_SCHEDULER: '1' }
|
|
||||||
: process.env
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit', env })
|
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' })
|
||||||
child.on('close', () => resolve())
|
child.on('close', () => resolve())
|
||||||
child.on('error', () => resolve())
|
child.on('error', () => resolve())
|
||||||
})
|
})
|
||||||
@@ -1143,13 +1146,24 @@ export class MicrosoftRewardsBot {
|
|||||||
private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> {
|
private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||||
await ConclusionWebhook(
|
const title = '🚨 Global security standby engaged'
|
||||||
this.config,
|
const desc = [
|
||||||
'🚨 Global Security Standby Engaged',
|
`Account: ${email}`,
|
||||||
`@everyone\n\n**Account:** ${email}\n**Reason:** ${reason}\n**Action:** Pausing all further accounts. We will not proceed until this is resolved.\n\n_Security check by @Light_`,
|
`Reason: ${reason}`,
|
||||||
undefined,
|
'Action: Pausing all further accounts. We will not proceed until this is resolved.',
|
||||||
DISCORD.COLOR_RED
|
'Security check by @Light'
|
||||||
)
|
].join('\n')
|
||||||
|
// Mention everyone in content for Discord visibility
|
||||||
|
const content = '@everyone ' + title
|
||||||
|
await ConclusionWebhook(this.config, content, {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
description: desc,
|
||||||
|
color: DISCORD.COLOR_RED
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('main','ALERT',`Failed to send standby alert: ${e instanceof Error ? e.message : e}`,'warn')
|
log('main','ALERT',`Failed to send standby alert: ${e instanceof Error ? e.message : e}`,'warn')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export interface Account {
|
export interface Account {
|
||||||
/** Enable/disable this account (if false, account will be skipped during execution) */
|
|
||||||
enabled?: boolean;
|
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
/** Optional TOTP secret in Base32 (e.g., from Microsoft Authenticator setup) */
|
/** Optional TOTP secret in Base32 (e.g., from Microsoft Authenticator setup) */
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export interface ConfigSearchDelay {
|
|||||||
export interface ConfigWebhook {
|
export interface ConfigWebhook {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
username?: string; // Custom webhook username (default: "Microsoft Rewards")
|
username?: string; // Optional override for displayed webhook name
|
||||||
avatarUrl?: string; // Custom webhook avatar URL
|
avatarUrl?: string; // Optional avatar image URL
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigNtfy {
|
export interface ConfigNtfy {
|
||||||
@@ -95,8 +95,6 @@ export interface ConfigUpdate {
|
|||||||
git?: boolean; // if true, run git pull + npm ci + npm run build after completion
|
git?: boolean; // if true, run git pull + npm ci + npm run build after completion
|
||||||
docker?: boolean; // if true, run docker update routine (compose pull/up) after completion
|
docker?: boolean; // if true, run docker update routine (compose pull/up) after completion
|
||||||
scriptPath?: string; // optional custom path to update script relative to repo root
|
scriptPath?: string; // optional custom path to update script relative to repo root
|
||||||
autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings)
|
|
||||||
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigBuyMode {
|
export interface ConfigBuyMode {
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ type DateTimeInstance = ReturnType<typeof DateTime.fromJSDate>
|
|||||||
|
|
||||||
function resolveTimeParts(schedule: Config['schedule'] | undefined): { tz: string; hour: number; minute: number } {
|
function resolveTimeParts(schedule: Config['schedule'] | undefined): { tz: string; hour: number; minute: number } {
|
||||||
const tz = (schedule?.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : 'UTC'
|
const tz = (schedule?.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : 'UTC'
|
||||||
|
|
||||||
// Warn if an invalid timezone was provided
|
|
||||||
if (schedule?.timeZone && !IANAZone.isValidZone(schedule.timeZone)) {
|
|
||||||
void log('main', 'SCHEDULER', `Invalid timezone "${schedule.timeZone}" provided. Falling back to UTC. Valid zones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones`, 'warn')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine source string
|
// Determine source string
|
||||||
let src = ''
|
let src = ''
|
||||||
if (typeof schedule?.useAmPm === 'boolean') {
|
if (typeof schedule?.useAmPm === 'boolean') {
|
||||||
@@ -120,28 +114,13 @@ async function runOnePassWithWatchdog(): Promise<void> {
|
|||||||
// Heartbeat-aware watchdog configuration
|
// Heartbeat-aware watchdog configuration
|
||||||
// If a child is actively updating its heartbeat file, we allow it to run beyond the legacy timeout.
|
// If a child is actively updating its heartbeat file, we allow it to run beyond the legacy timeout.
|
||||||
// Defaults are generous to allow first-day passes to finish searches with delays.
|
// Defaults are generous to allow first-day passes to finish searches with delays.
|
||||||
const parseEnvNumber = (key: string, fallback: number, min: number, max: number): number => {
|
const staleHeartbeatMin = Number(
|
||||||
const val = Number(process.env[key] || fallback)
|
process.env.SCHEDULER_STALE_HEARTBEAT_MINUTES || process.env.SCHEDULER_PASS_TIMEOUT_MINUTES || 30
|
||||||
if (isNaN(val) || val < min || val > max) {
|
|
||||||
void log('main', 'SCHEDULER', `Invalid ${key}="${process.env[key]}". Using default ${fallback}`, 'warn')
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
const staleHeartbeatMin = parseEnvNumber(
|
|
||||||
process.env.SCHEDULER_STALE_HEARTBEAT_MINUTES ? 'SCHEDULER_STALE_HEARTBEAT_MINUTES' : 'SCHEDULER_PASS_TIMEOUT_MINUTES',
|
|
||||||
30, 5, 1440
|
|
||||||
)
|
)
|
||||||
const graceMin = parseEnvNumber('SCHEDULER_HEARTBEAT_GRACE_MINUTES', 15, 1, 120)
|
const graceMin = Number(process.env.SCHEDULER_HEARTBEAT_GRACE_MINUTES || 15)
|
||||||
const hardcapMin = parseEnvNumber('SCHEDULER_PASS_HARDCAP_MINUTES', 480, 30, 1440)
|
const hardcapMin = Number(process.env.SCHEDULER_PASS_HARDCAP_MINUTES || 480) // 8 hours
|
||||||
const checkEveryMs = 60_000 // check once per minute
|
const checkEveryMs = 60_000 // check once per minute
|
||||||
|
|
||||||
// Validate: stale should be >= grace
|
|
||||||
if (staleHeartbeatMin < graceMin) {
|
|
||||||
await log('main', 'SCHEDULER', `Warning: STALE_HEARTBEAT (${staleHeartbeatMin}m) < GRACE (${graceMin}m). Adjusting stale to ${graceMin}m`, 'warn')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fork per pass: safer because we can terminate a stuck child without killing the scheduler
|
// Fork per pass: safer because we can terminate a stuck child without killing the scheduler
|
||||||
const forkPerPass = String(process.env.SCHEDULER_FORK_PER_PASS || 'true').toLowerCase() !== 'false'
|
const forkPerPass = String(process.env.SCHEDULER_FORK_PER_PASS || 'true').toLowerCase() !== 'false'
|
||||||
|
|
||||||
@@ -168,8 +147,6 @@ async function runOnePassWithWatchdog(): Promise<void> {
|
|||||||
let finished = false
|
let finished = false
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
|
|
||||||
let killTimeout: NodeJS.Timeout | undefined
|
|
||||||
|
|
||||||
const killChild = async (signal: NodeJS.Signals) => {
|
const killChild = async (signal: NodeJS.Signals) => {
|
||||||
try {
|
try {
|
||||||
await log('main', 'SCHEDULER', `Sending ${signal} to stuck child PID ${child.pid}`,'warn')
|
await log('main', 'SCHEDULER', `Sending ${signal} to stuck child PID ${child.pid}`,'warn')
|
||||||
@@ -185,8 +162,7 @@ async function runOnePassWithWatchdog(): Promise<void> {
|
|||||||
if (runtimeMin >= hardcapMin) {
|
if (runtimeMin >= hardcapMin) {
|
||||||
log('main', 'SCHEDULER', `Pass exceeded hard cap of ${hardcapMin} minutes; terminating...`, 'warn')
|
log('main', 'SCHEDULER', `Pass exceeded hard cap of ${hardcapMin} minutes; terminating...`, 'warn')
|
||||||
void killChild('SIGTERM')
|
void killChild('SIGTERM')
|
||||||
if (killTimeout) clearTimeout(killTimeout)
|
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
||||||
killTimeout = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Before grace, don't judge
|
// Before grace, don't judge
|
||||||
@@ -199,23 +175,19 @@ async function runOnePassWithWatchdog(): Promise<void> {
|
|||||||
if (ageMin >= staleHeartbeatMin) {
|
if (ageMin >= staleHeartbeatMin) {
|
||||||
log('main', 'SCHEDULER', `Heartbeat stale for ${ageMin}m (>=${staleHeartbeatMin}m). Terminating child...`, 'warn')
|
log('main', 'SCHEDULER', `Heartbeat stale for ${ageMin}m (>=${staleHeartbeatMin}m). Terminating child...`, 'warn')
|
||||||
void killChild('SIGTERM')
|
void killChild('SIGTERM')
|
||||||
if (killTimeout) clearTimeout(killTimeout)
|
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
||||||
killTimeout = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
// If file missing after grace, consider stale
|
// If file missing after grace, consider stale
|
||||||
const msg = err instanceof Error ? err.message : String(err)
|
log('main', 'SCHEDULER', 'Heartbeat file missing after grace. Terminating child...', 'warn')
|
||||||
log('main', 'SCHEDULER', `Heartbeat file check failed: ${msg}. Terminating child...`, 'warn')
|
|
||||||
void killChild('SIGTERM')
|
void killChild('SIGTERM')
|
||||||
if (killTimeout) clearTimeout(killTimeout)
|
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
||||||
killTimeout = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
|
|
||||||
}
|
}
|
||||||
}, checkEveryMs)
|
}, checkEveryMs)
|
||||||
|
|
||||||
child.on('exit', async (code, signal) => {
|
child.on('exit', async (code, signal) => {
|
||||||
finished = true
|
finished = true
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
if (killTimeout) clearTimeout(killTimeout)
|
|
||||||
// Cleanup heartbeat file
|
// Cleanup heartbeat file
|
||||||
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
|
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
|
||||||
if (signal) {
|
if (signal) {
|
||||||
@@ -231,7 +203,6 @@ async function runOnePassWithWatchdog(): Promise<void> {
|
|||||||
child.on('error', async (err) => {
|
child.on('error', async (err) => {
|
||||||
finished = true
|
finished = true
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
if (killTimeout) clearTimeout(killTimeout)
|
|
||||||
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
|
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
|
||||||
await log('main', 'SCHEDULER', `Failed to spawn child: ${err instanceof Error ? err.message : String(err)}`, 'error')
|
await log('main', 'SCHEDULER', `Failed to spawn child: ${err instanceof Error ? err.message : String(err)}`, 'error')
|
||||||
resolve()
|
resolve()
|
||||||
@@ -315,21 +286,9 @@ async function main() {
|
|||||||
let running = false
|
let running = false
|
||||||
|
|
||||||
// Optional initial jitter before the first run (to vary start time)
|
// Optional initial jitter before the first run (to vary start time)
|
||||||
const parseJitter = (minKey: string, maxKey: string, fallbackMin: string, fallbackMax: string): [number, number] => {
|
const initJitterMin = Number(process.env.SCHEDULER_INITIAL_JITTER_MINUTES_MIN || process.env.SCHEDULER_INITIAL_JITTER_MIN || 0)
|
||||||
const minVal = Number(process.env[minKey] || process.env[fallbackMin] || 0)
|
const initJitterMax = Number(process.env.SCHEDULER_INITIAL_JITTER_MINUTES_MAX || process.env.SCHEDULER_INITIAL_JITTER_MAX || 0)
|
||||||
const maxVal = Number(process.env[maxKey] || process.env[fallbackMax] || 0)
|
const initialJitterBounds: [number, number] = [isFinite(initJitterMin) ? initJitterMin : 0, isFinite(initJitterMax) ? initJitterMax : 0]
|
||||||
if (isNaN(minVal) || minVal < 0) {
|
|
||||||
void log('main', 'SCHEDULER', `Invalid ${minKey}="${process.env[minKey]}". Using 0`, 'warn')
|
|
||||||
return [0, isNaN(maxVal) || maxVal < 0 ? 0 : maxVal]
|
|
||||||
}
|
|
||||||
if (isNaN(maxVal) || maxVal < 0) {
|
|
||||||
void log('main', 'SCHEDULER', `Invalid ${maxKey}="${process.env[maxKey]}". Using 0`, 'warn')
|
|
||||||
return [minVal, 0]
|
|
||||||
}
|
|
||||||
return [minVal, maxVal]
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialJitterBounds = parseJitter('SCHEDULER_INITIAL_JITTER_MINUTES_MIN', 'SCHEDULER_INITIAL_JITTER_MINUTES_MAX', 'SCHEDULER_INITIAL_JITTER_MIN', 'SCHEDULER_INITIAL_JITTER_MAX')
|
|
||||||
const applyInitialJitter = (initialJitterBounds[0] > 0 || initialJitterBounds[1] > 0)
|
const applyInitialJitter = (initialJitterBounds[0] > 0 || initialJitterBounds[1] > 0)
|
||||||
|
|
||||||
if (runImmediate && !running) {
|
if (runImmediate && !running) {
|
||||||
@@ -368,9 +327,10 @@ async function main() {
|
|||||||
// Optional daily jitter to further randomize the exact start time each day
|
// Optional daily jitter to further randomize the exact start time each day
|
||||||
let extraMs = 0
|
let extraMs = 0
|
||||||
if (cronExpressions.length === 0) {
|
if (cronExpressions.length === 0) {
|
||||||
const dailyJitterBounds = parseJitter('SCHEDULER_DAILY_JITTER_MINUTES_MIN', 'SCHEDULER_DAILY_JITTER_MINUTES_MAX', 'SCHEDULER_DAILY_JITTER_MIN', 'SCHEDULER_DAILY_JITTER_MAX')
|
const dailyJitterMin = Number(process.env.SCHEDULER_DAILY_JITTER_MINUTES_MIN || process.env.SCHEDULER_DAILY_JITTER_MIN || 0)
|
||||||
const djMin = dailyJitterBounds[0]
|
const dailyJitterMax = Number(process.env.SCHEDULER_DAILY_JITTER_MINUTES_MAX || process.env.SCHEDULER_DAILY_JITTER_MAX || 0)
|
||||||
const djMax = dailyJitterBounds[1]
|
const djMin = isFinite(dailyJitterMin) ? dailyJitterMin : 0
|
||||||
|
const djMax = isFinite(dailyJitterMax) ? dailyJitterMax : 0
|
||||||
if (djMin > 0 || djMax > 0) {
|
if (djMin > 0 || djMax > 0) {
|
||||||
const mn = Math.max(0, Math.min(djMin, djMax))
|
const mn = Math.max(0, Math.min(djMin, djMax))
|
||||||
const mx = Math.max(0, Math.max(djMin, djMax))
|
const mx = Math.max(0, Math.max(djMin, djMax))
|
||||||
@@ -413,6 +373,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
void log('main', 'SCHEDULER', `Fatal error: ${e instanceof Error ? e.message : String(e)}`, 'error')
|
console.error(e)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -202,11 +202,6 @@ export class Analytics {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional community info footer (only in markdown exports)
|
|
||||||
lines.push('')
|
|
||||||
lines.push('---')
|
|
||||||
lines.push('*Looking for faster updates, advanced features, and better anti-detection? Join our community for enhanced versions and support.*')
|
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ class AxiosClient {
|
|||||||
const { url, port } = proxyConfig
|
const { url, port } = proxyConfig
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case proxyConfig.url.startsWith('http://'):
|
case proxyConfig.url.startsWith('http'):
|
||||||
return new HttpProxyAgent(`${url}:${port}`)
|
return new HttpProxyAgent(`${url}:${port}`)
|
||||||
case proxyConfig.url.startsWith('https://'):
|
case proxyConfig.url.startsWith('https'):
|
||||||
return new HttpsProxyAgent(`${url}:${port}`)
|
return new HttpsProxyAgent(`${url}:${port}`)
|
||||||
case proxyConfig.url.startsWith('socks://') || proxyConfig.url.startsWith('socks4://') || proxyConfig.url.startsWith('socks5://'):
|
case proxyConfig.url.startsWith('socks'):
|
||||||
return new SocksProxyAgent(`${url}:${port}`)
|
return new SocksProxyAgent(`${url}:${port}`)
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported proxy protocol in "${url}". Supported: http://, https://, socks://, socks4://, socks5://`)
|
throw new Error(`Unsupported proxy protocol: ${url}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,54 +42,29 @@ class AxiosClient {
|
|||||||
return bypassInstance.request(config)
|
return bypassInstance.request(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastError: unknown
|
try {
|
||||||
const maxAttempts = 2
|
return await this.instance.request(config)
|
||||||
|
} catch (err: unknown) {
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
const axiosErr = err as AxiosError | undefined
|
||||||
try {
|
|
||||||
return await this.instance.request(config)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
lastError = err
|
|
||||||
const axiosErr = err as AxiosError | undefined
|
|
||||||
|
|
||||||
// Detect HTTP proxy auth failures (status 407) and retry without proxy
|
// Detect HTTP proxy auth failures (status 407) and retry without proxy once.
|
||||||
if (axiosErr && axiosErr.response && axiosErr.response.status === 407) {
|
if (!bypassProxy && axiosErr && axiosErr.response && axiosErr.response.status === 407) {
|
||||||
if (attempt < maxAttempts) {
|
const bypassInstance = axios.create()
|
||||||
await this.sleep(1000 * attempt) // Exponential backoff
|
return bypassInstance.request(config)
|
||||||
}
|
|
||||||
const bypassInstance = axios.create()
|
|
||||||
return bypassInstance.request(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If proxied request fails with common proxy/network errors, retry with backoff
|
|
||||||
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
|
|
||||||
const code = e?.code || e?.cause?.code
|
|
||||||
const isNetErr = code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ENOTFOUND'
|
|
||||||
const msg = String(e?.message || '')
|
|
||||||
const looksLikeProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
|
||||||
|
|
||||||
if (isNetErr || looksLikeProxyIssue) {
|
|
||||||
if (attempt < maxAttempts) {
|
|
||||||
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
||||||
const delayMs = 1000 * Math.pow(2, attempt - 1)
|
|
||||||
await this.sleep(delayMs)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Last attempt: try without proxy
|
|
||||||
const bypassInstance = axios.create()
|
|
||||||
return bypassInstance.request(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-retryable error
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If proxied request fails with common proxy/network errors, retry once without proxy
|
||||||
|
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
|
||||||
|
const code = e?.code || e?.cause?.code
|
||||||
|
const isNetErr = code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ENOTFOUND'
|
||||||
|
const msg = String(e?.message || '')
|
||||||
|
const looksLikeProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
||||||
|
if (!bypassProxy && (isNetErr || looksLikeProxyIssue)) {
|
||||||
|
const bypassInstance = axios.create()
|
||||||
|
return bypassInstance.request(config)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError
|
|
||||||
}
|
|
||||||
|
|
||||||
private sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,358 +1,106 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Config } from '../interface/Config'
|
import { Config } from '../interface/Config'
|
||||||
import { Ntfy } from './Ntfy'
|
import { Ntfy } from './Ntfy'
|
||||||
import { DISCORD } from '../constants'
|
|
||||||
import { log } from './Logger'
|
|
||||||
|
|
||||||
interface DiscordField {
|
// Avatar URL for webhook (new clean logo)
|
||||||
name: string
|
const AVATAR_URL = 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
||||||
value: string
|
|
||||||
inline?: boolean
|
type WebhookContext = 'summary' | 'ban' | 'security' | 'compromised' | 'spend' | 'error' | 'default'
|
||||||
|
|
||||||
|
function pickUsername(ctx: WebhookContext, fallbackColor?: number): string {
|
||||||
|
switch (ctx) {
|
||||||
|
case 'summary': return 'MS Rewards - Daily Summary'
|
||||||
|
case 'ban': return 'MS Rewards - Ban Detected'
|
||||||
|
case 'security': return 'MS Rewards - Security Alert'
|
||||||
|
case 'compromised': return 'MS Rewards - Account Compromised'
|
||||||
|
case 'spend': return 'MS Rewards - Purchase Notification'
|
||||||
|
case 'error': return 'MS Rewards - Error Report'
|
||||||
|
default: return fallbackColor === 0xFF0000 ? 'MS Rewards - Error Report' : 'MS Rewards Bot'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DiscordField { name: string; value: string; inline?: boolean }
|
||||||
interface DiscordEmbed {
|
interface DiscordEmbed {
|
||||||
title?: string
|
title?: string
|
||||||
description?: string
|
description?: string
|
||||||
color?: number
|
color?: number
|
||||||
fields?: DiscordField[]
|
fields?: DiscordField[]
|
||||||
timestamp?: string
|
|
||||||
footer?: {
|
|
||||||
text: string
|
|
||||||
icon_url?: string
|
|
||||||
}
|
|
||||||
thumbnail?: {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
author?: {
|
|
||||||
name: string
|
|
||||||
icon_url?: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebhookPayload {
|
interface ConclusionPayload {
|
||||||
username: string
|
content?: string
|
||||||
avatar_url: string
|
embeds?: DiscordEmbed[]
|
||||||
embeds: DiscordEmbed[]
|
context?: WebhookContext
|
||||||
}
|
|
||||||
|
|
||||||
interface AccountSummary {
|
|
||||||
email: string
|
|
||||||
totalCollected: number
|
|
||||||
desktopCollected: number
|
|
||||||
mobileCollected: number
|
|
||||||
initialTotal: number
|
|
||||||
endTotal: number
|
|
||||||
durationMs: number
|
|
||||||
errors: string[]
|
|
||||||
banned?: { status: boolean; reason?: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConclusionData {
|
|
||||||
version: string
|
|
||||||
runId: string
|
|
||||||
totalAccounts: number
|
|
||||||
successes: number
|
|
||||||
accountsWithErrors: number
|
|
||||||
accountsBanned: number
|
|
||||||
totalCollected: number
|
|
||||||
totalInitial: number
|
|
||||||
totalEnd: number
|
|
||||||
avgPointsPerAccount: number
|
|
||||||
totalDuration: number
|
|
||||||
avgDuration: number
|
|
||||||
summaries: AccountSummary[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a clean, structured Discord webhook notification
|
* Send a final structured summary to the configured webhook,
|
||||||
|
* and optionally mirror a plain-text summary to NTFY.
|
||||||
|
*
|
||||||
|
* This preserves existing webhook behavior while adding NTFY
|
||||||
|
* as a separate, optional channel.
|
||||||
*/
|
*/
|
||||||
export async function ConclusionWebhook(
|
export async function ConclusionWebhook(config: Config, content: string, payload?: ConclusionPayload) {
|
||||||
config: Config,
|
// Send to both webhooks when available
|
||||||
title: string,
|
const hasConclusion = !!(config.conclusionWebhook?.enabled && config.conclusionWebhook.url)
|
||||||
description: string,
|
const hasWebhook = !!(config.webhook?.enabled && config.webhook.url)
|
||||||
fields?: DiscordField[],
|
const sameTarget = hasConclusion && hasWebhook && config.conclusionWebhook!.url === config.webhook!.url
|
||||||
color?: number
|
|
||||||
) {
|
|
||||||
const hasConclusion = config.conclusionWebhook?.enabled && config.conclusionWebhook.url
|
|
||||||
const hasWebhook = config.webhook?.enabled && config.webhook.url
|
|
||||||
|
|
||||||
if (!hasConclusion && !hasWebhook) return
|
const body: ConclusionPayload & { username?: string; avatar_url?: string } = {}
|
||||||
|
if (payload?.embeds) body.embeds = payload.embeds
|
||||||
|
if (content && content.trim()) body.content = content
|
||||||
|
const firstColor = payload?.embeds && payload.embeds[0]?.color
|
||||||
|
const ctx: WebhookContext = payload?.context || (firstColor === 0xFF0000 ? 'error' : 'default')
|
||||||
|
body.username = pickUsername(ctx, firstColor)
|
||||||
|
body.avatar_url = AVATAR_URL
|
||||||
|
|
||||||
const embed: DiscordEmbed = {
|
// Post to conclusion webhook if configured
|
||||||
title,
|
const postWithRetry = async (url: string, label: string) => {
|
||||||
description,
|
const max = 2
|
||||||
color: color || 0x0078D4,
|
let lastErr: unknown = null
|
||||||
timestamp: new Date().toISOString()
|
for (let attempt = 1; attempt <= max; attempt++) {
|
||||||
}
|
|
||||||
|
|
||||||
if (fields && fields.length > 0) {
|
|
||||||
embed.fields = fields
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use custom webhook settings if provided, otherwise fall back to defaults
|
|
||||||
const webhookUsername = config.webhook?.username || config.conclusionWebhook?.username || 'Microsoft Rewards'
|
|
||||||
const webhookAvatarUrl = config.webhook?.avatarUrl || config.conclusionWebhook?.avatarUrl || DISCORD.AVATAR_URL
|
|
||||||
|
|
||||||
const payload: WebhookPayload = {
|
|
||||||
username: webhookUsername,
|
|
||||||
avatar_url: webhookAvatarUrl,
|
|
||||||
embeds: [embed]
|
|
||||||
}
|
|
||||||
|
|
||||||
const postWebhook = async (url: string, label: string) => {
|
|
||||||
const maxAttempts = 3
|
|
||||||
let lastError: unknown = null
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
try {
|
try {
|
||||||
await axios.post(url, payload, {
|
await axios.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 })
|
||||||
headers: { 'Content-Type': 'application/json' },
|
console.log(`[Webhook:${label}] summary sent (attempt ${attempt}).`)
|
||||||
timeout: 15000
|
|
||||||
})
|
|
||||||
log('main', 'WEBHOOK', `${label} notification sent successfully (attempt ${attempt})`)
|
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
lastError = error
|
lastErr = e
|
||||||
if (attempt < maxAttempts) {
|
if (attempt === max) break
|
||||||
// Exponential backoff: 1s, 2s, 4s
|
await new Promise(r => setTimeout(r, 1000 * attempt))
|
||||||
const delayMs = 1000 * Math.pow(2, attempt - 1)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log('main', 'WEBHOOK', `${label} failed after ${maxAttempts} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, 'error')
|
console.error(`[Webhook:${label}] failed after ${max} attempts:`, lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
const urls = new Set<string>()
|
if (hasConclusion) {
|
||||||
if (hasConclusion) urls.add(config.conclusionWebhook!.url)
|
await postWithRetry(config.conclusionWebhook!.url, sameTarget ? 'conclusion+primary' : 'conclusion')
|
||||||
if (hasWebhook) urls.add(config.webhook!.url)
|
}
|
||||||
|
if (hasWebhook && !sameTarget) {
|
||||||
|
await postWithRetry(config.webhook!.url, 'primary')
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
// NTFY: mirror a plain text summary (optional)
|
||||||
Array.from(urls).map((url, index) => postWebhook(url, `webhook-${index + 1}`))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Optional NTFY notification
|
|
||||||
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
|
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
|
||||||
const message = `${title}\n${description}${fields ? '\n\n' + fields.map(f => `${f.name}: ${f.value}`).join('\n') : ''}`
|
let message = content || ''
|
||||||
const ntfyType = color === 0xFF0000 ? 'error' : color === 0xFFAA00 ? 'warn' : 'log'
|
if (!message && payload?.embeds && payload.embeds.length > 0) {
|
||||||
|
const e: DiscordEmbed = payload.embeds[0]!
|
||||||
|
const title = e.title ? `${e.title}\n` : ''
|
||||||
|
const desc = e.description ? `${e.description}\n` : ''
|
||||||
|
const totals = e.fields && e.fields[0]?.value ? `\n${e.fields[0].value}\n` : ''
|
||||||
|
message = `${title}${desc}${totals}`.trim()
|
||||||
|
}
|
||||||
|
if (!message) message = 'Microsoft Rewards run complete.'
|
||||||
|
// Choose NTFY level based on embed color (yellow = warn)
|
||||||
|
let embedColor: number | undefined
|
||||||
|
if (payload?.embeds && payload.embeds.length > 0) {
|
||||||
|
embedColor = payload.embeds[0]!.color
|
||||||
|
}
|
||||||
|
const ntfyType = embedColor === 0xFFAA00 ? 'warn' : 'log'
|
||||||
try {
|
try {
|
||||||
await Ntfy(message, ntfyType)
|
await Ntfy(message, ntfyType)
|
||||||
log('main', 'NTFY', 'Notification sent successfully')
|
console.log('Conclusion summary sent to NTFY.')
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
log('main', 'NTFY', `Failed to send notification: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
console.error('Failed to send conclusion summary to NTFY:', err)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced conclusion webhook with beautiful formatting and clear statistics
|
|
||||||
*/
|
|
||||||
export async function ConclusionWebhookEnhanced(config: Config, data: ConclusionData) {
|
|
||||||
const hasConclusion = config.conclusionWebhook?.enabled && config.conclusionWebhook.url
|
|
||||||
const hasWebhook = config.webhook?.enabled && config.webhook.url
|
|
||||||
|
|
||||||
if (!hasConclusion && !hasWebhook) return
|
|
||||||
|
|
||||||
// Helper to format duration
|
|
||||||
const formatDuration = (ms: number): string => {
|
|
||||||
const totalSeconds = Math.floor(ms / 1000)
|
|
||||||
const hours = Math.floor(totalSeconds / 3600)
|
|
||||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
||||||
const seconds = totalSeconds % 60
|
|
||||||
|
|
||||||
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
|
|
||||||
if (minutes > 0) return `${minutes}m ${seconds}s`
|
|
||||||
return `${seconds}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create progress bar (future use)
|
|
||||||
// const createProgressBar = (current: number, max: number, length: number = 10): string => {
|
|
||||||
// const percentage = Math.min(100, Math.max(0, (current / max) * 100))
|
|
||||||
// const filled = Math.round((percentage / 100) * length)
|
|
||||||
// const empty = length - filled
|
|
||||||
// return `${'█'.repeat(filled)}${'░'.repeat(empty)} ${percentage.toFixed(0)}%`
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Determine overall status and color
|
|
||||||
let statusEmoji = '✅'
|
|
||||||
let statusText = 'Success'
|
|
||||||
let embedColor: number = DISCORD.COLOR_GREEN
|
|
||||||
|
|
||||||
if (data.accountsBanned > 0) {
|
|
||||||
statusEmoji = '🚫'
|
|
||||||
statusText = 'Banned Accounts Detected'
|
|
||||||
embedColor = DISCORD.COLOR_RED
|
|
||||||
} else if (data.accountsWithErrors > 0) {
|
|
||||||
statusEmoji = '⚠️'
|
|
||||||
statusText = 'Completed with Warnings'
|
|
||||||
embedColor = DISCORD.COLOR_ORANGE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build main summary description
|
|
||||||
const mainDescription = [
|
|
||||||
`**Status:** ${statusEmoji} ${statusText}`,
|
|
||||||
`**Version:** v${data.version} • **Run ID:** \`${data.runId}\``,
|
|
||||||
'',
|
|
||||||
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
// Build global statistics field
|
|
||||||
const globalStats = [
|
|
||||||
`**💎 Total Points Earned**`,
|
|
||||||
`\`${data.totalInitial.toLocaleString()}\` → \`${data.totalEnd.toLocaleString()}\` **(+${data.totalCollected.toLocaleString()})**`,
|
|
||||||
'',
|
|
||||||
`**📊 Accounts Processed**`,
|
|
||||||
`✅ Success: **${data.successes}** | ⚠️ Errors: **${data.accountsWithErrors}** | 🚫 Banned: **${data.accountsBanned}**`,
|
|
||||||
`Total: **${data.totalAccounts}** ${data.totalAccounts === 1 ? 'account' : 'accounts'}`,
|
|
||||||
'',
|
|
||||||
`**⚡ Performance**`,
|
|
||||||
`Average: **${data.avgPointsPerAccount}pts/account** in **${formatDuration(data.avgDuration)}**`,
|
|
||||||
`Total Runtime: **${formatDuration(data.totalDuration)}**`
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
// Build per-account breakdown (split if too many accounts)
|
|
||||||
const accountFields: DiscordField[] = []
|
|
||||||
const maxAccountsPerField = 5
|
|
||||||
const accountChunks: AccountSummary[][] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < data.summaries.length; i += maxAccountsPerField) {
|
|
||||||
accountChunks.push(data.summaries.slice(i, i + maxAccountsPerField))
|
|
||||||
}
|
|
||||||
|
|
||||||
accountChunks.forEach((chunk, chunkIndex) => {
|
|
||||||
const accountLines: string[] = []
|
|
||||||
|
|
||||||
chunk.forEach((acc) => {
|
|
||||||
const statusIcon = acc.banned?.status ? '🚫' : (acc.errors.length > 0 ? '⚠️' : '✅')
|
|
||||||
const emailShort = acc.email.length > 25 ? acc.email.substring(0, 22) + '...' : acc.email
|
|
||||||
|
|
||||||
accountLines.push(`${statusIcon} **${emailShort}**`)
|
|
||||||
accountLines.push(`└ Points: **+${acc.totalCollected}** (🖥️ ${acc.desktopCollected} • 📱 ${acc.mobileCollected})`)
|
|
||||||
accountLines.push(`└ Duration: ${formatDuration(acc.durationMs)}`)
|
|
||||||
|
|
||||||
if (acc.banned?.status) {
|
|
||||||
accountLines.push(`└ 🚫 **Banned:** ${acc.banned.reason || 'Account suspended'}`)
|
|
||||||
} else if (acc.errors.length > 0) {
|
|
||||||
const errorPreview = acc.errors.slice(0, 1).join(', ')
|
|
||||||
accountLines.push(`└ ⚠️ **Error:** ${errorPreview.length > 50 ? errorPreview.substring(0, 47) + '...' : errorPreview}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
accountLines.push('') // Empty line between accounts
|
|
||||||
})
|
|
||||||
|
|
||||||
const fieldName = accountChunks.length > 1
|
|
||||||
? `📈 Account Details (${chunkIndex + 1}/${accountChunks.length})`
|
|
||||||
: '📈 Account Details'
|
|
||||||
|
|
||||||
accountFields.push({
|
|
||||||
name: fieldName,
|
|
||||||
value: accountLines.join('\n').trim(),
|
|
||||||
inline: false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create embeds
|
|
||||||
const embeds: DiscordEmbed[] = []
|
|
||||||
|
|
||||||
// Main embed with summary
|
|
||||||
embeds.push({
|
|
||||||
title: '🎯 Microsoft Rewards — Daily Summary',
|
|
||||||
description: mainDescription,
|
|
||||||
color: embedColor,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: '📊 Global Statistics',
|
|
||||||
value: globalStats,
|
|
||||||
inline: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
thumbnail: {
|
|
||||||
url: 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
text: `Microsoft Rewards Bot v${data.version} • Completed at`,
|
|
||||||
icon_url: 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add account details in separate embed(s) if needed
|
|
||||||
if (accountFields.length > 0) {
|
|
||||||
// If we have multiple fields, split into multiple embeds
|
|
||||||
accountFields.forEach((field, index) => {
|
|
||||||
if (index === 0 && embeds[0] && embeds[0].fields) {
|
|
||||||
// Add first field to main embed
|
|
||||||
embeds[0].fields.push(field)
|
|
||||||
} else {
|
|
||||||
// Create additional embeds for remaining fields
|
|
||||||
embeds.push({
|
|
||||||
color: embedColor,
|
|
||||||
fields: [field],
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use custom webhook settings
|
|
||||||
const webhookUsername = config.conclusionWebhook?.username || config.webhook?.username || 'Microsoft Rewards'
|
|
||||||
const webhookAvatarUrl = config.conclusionWebhook?.avatarUrl || config.webhook?.avatarUrl || DISCORD.AVATAR_URL
|
|
||||||
|
|
||||||
const payload: WebhookPayload = {
|
|
||||||
username: webhookUsername,
|
|
||||||
avatar_url: webhookAvatarUrl,
|
|
||||||
embeds
|
|
||||||
}
|
|
||||||
|
|
||||||
const postWebhook = async (url: string, label: string) => {
|
|
||||||
const maxAttempts = 3
|
|
||||||
let lastError: unknown = null
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
try {
|
|
||||||
await axios.post(url, payload, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
timeout: 15000
|
|
||||||
})
|
|
||||||
log('main', 'WEBHOOK', `${label} conclusion sent successfully (${data.totalAccounts} accounts, +${data.totalCollected}pts)`)
|
|
||||||
return
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error
|
|
||||||
if (attempt < maxAttempts) {
|
|
||||||
const delayMs = 1000 * Math.pow(2, attempt - 1)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log('main', 'WEBHOOK', `${label} failed after ${maxAttempts} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`, 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
const urls = new Set<string>()
|
|
||||||
if (hasConclusion) urls.add(config.conclusionWebhook!.url)
|
|
||||||
if (hasWebhook) urls.add(config.webhook!.url)
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(urls).map((url, index) => postWebhook(url, `conclusion-webhook-${index + 1}`))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Optional NTFY notification (simplified summary)
|
|
||||||
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
|
|
||||||
const message = [
|
|
||||||
`🎯 Microsoft Rewards Summary`,
|
|
||||||
`Status: ${statusText}`,
|
|
||||||
`Points: ${data.totalInitial} → ${data.totalEnd} (+${data.totalCollected})`,
|
|
||||||
`Accounts: ${data.successes}/${data.totalAccounts} successful`,
|
|
||||||
`Duration: ${formatDuration(data.totalDuration)}`
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const ntfyType = embedColor === DISCORD.COLOR_RED ? 'error' : embedColor === DISCORD.COLOR_ORANGE ? 'warn' : 'log'
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Ntfy(message, ntfyType)
|
|
||||||
log('main', 'NTFY', 'Conclusion notification sent successfully')
|
|
||||||
} catch (error) {
|
|
||||||
log('main', 'NTFY', `Failed to send conclusion notification: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,7 +470,6 @@ export class ConfigValidator {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Print validation results to console with color
|
* Print validation results to console with color
|
||||||
* Note: This method intentionally uses console.log for CLI output formatting
|
|
||||||
*/
|
*/
|
||||||
static printResults(result: ValidationResult): void {
|
static printResults(result: ValidationResult): void {
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
|
|||||||
@@ -215,18 +215,12 @@ export function loadAccounts(): Account[] {
|
|||||||
raw = fs.readFileSync(full, 'utf-8')
|
raw = fs.readFileSync(full, 'utf-8')
|
||||||
} else {
|
} else {
|
||||||
// Try multiple locations to support both root mounts and dist mounts
|
// Try multiple locations to support both root mounts and dist mounts
|
||||||
// Support both .json and .jsonc extensions
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(__dirname, '../', file), // root/accounts.json (preferred)
|
path.join(__dirname, '../', file), // root/accounts.json (preferred)
|
||||||
path.join(__dirname, '../', file + 'c'), // root/accounts.jsonc
|
|
||||||
path.join(__dirname, '../src', file), // fallback: file kept inside src/
|
path.join(__dirname, '../src', file), // fallback: file kept inside src/
|
||||||
path.join(__dirname, '../src', file + 'c'), // src/accounts.jsonc
|
|
||||||
path.join(process.cwd(), file), // cwd override
|
path.join(process.cwd(), file), // cwd override
|
||||||
path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc
|
|
||||||
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
|
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
|
||||||
path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc
|
path.join(__dirname, file) // dist/accounts.json (legacy)
|
||||||
path.join(__dirname, file), // dist/accounts.json (legacy)
|
|
||||||
path.join(__dirname, file + 'c') // dist/accounts.jsonc
|
|
||||||
]
|
]
|
||||||
let chosen: string | null = null
|
let chosen: string | null = null
|
||||||
for (const p of candidates) {
|
for (const p of candidates) {
|
||||||
@@ -248,10 +242,7 @@ export function loadAccounts(): Account[] {
|
|||||||
throw new Error('each account must have email and password strings')
|
throw new Error('each account must have email and password strings')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Filter out disabled accounts (enabled: false)
|
return parsed as Account[]
|
||||||
const allAccounts = parsed as Account[]
|
|
||||||
const enabledAccounts = allAccounts.filter(acc => acc.enabled !== false)
|
|
||||||
return enabledAccounts
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error as string)
|
throw new Error(error as string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { Ntfy } from './Ntfy'
|
|||||||
import { loadConfig } from './Load'
|
import { loadConfig } from './Load'
|
||||||
import { DISCORD } from '../constants'
|
import { DISCORD } from '../constants'
|
||||||
|
|
||||||
const DEFAULT_LIVE_LOG_USERNAME = 'MS Rewards - Live Logs'
|
// Avatar URL for webhook (consistent with ConclusionWebhook)
|
||||||
|
const WEBHOOK_AVATAR_URL = 'https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png'
|
||||||
|
const WEBHOOK_USERNAME = 'MS Rewards - Live Logs'
|
||||||
|
|
||||||
type WebhookBuffer = {
|
type WebhookBuffer = {
|
||||||
lines: string[]
|
lines: string[]
|
||||||
@@ -15,41 +17,18 @@ type WebhookBuffer = {
|
|||||||
|
|
||||||
const webhookBuffers = new Map<string, WebhookBuffer>()
|
const webhookBuffers = new Map<string, WebhookBuffer>()
|
||||||
|
|
||||||
// Periodic cleanup of old/idle webhook buffers to prevent memory leaks
|
|
||||||
setInterval(() => {
|
|
||||||
const now = Date.now()
|
|
||||||
const BUFFER_MAX_AGE_MS = 3600000 // 1 hour
|
|
||||||
|
|
||||||
for (const [url, buf] of webhookBuffers.entries()) {
|
|
||||||
if (!buf.sending && buf.lines.length === 0) {
|
|
||||||
const lastActivity = (buf as unknown as { lastActivity?: number }).lastActivity || 0
|
|
||||||
if (now - lastActivity > BUFFER_MAX_AGE_MS) {
|
|
||||||
webhookBuffers.delete(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 600000) // Check every 10 minutes
|
|
||||||
|
|
||||||
function getBuffer(url: string): WebhookBuffer {
|
function getBuffer(url: string): WebhookBuffer {
|
||||||
let buf = webhookBuffers.get(url)
|
let buf = webhookBuffers.get(url)
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
buf = { lines: [], sending: false }
|
buf = { lines: [], sending: false }
|
||||||
webhookBuffers.set(url, buf)
|
webhookBuffers.set(url, buf)
|
||||||
}
|
}
|
||||||
// Track last activity for cleanup
|
|
||||||
(buf as unknown as { lastActivity: number }).lastActivity = Date.now()
|
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendBatch(url: string, buf: WebhookBuffer) {
|
async function sendBatch(url: string, buf: WebhookBuffer) {
|
||||||
if (buf.sending) return
|
if (buf.sending) return
|
||||||
buf.sending = true
|
buf.sending = true
|
||||||
|
|
||||||
// Load config to get webhook settings
|
|
||||||
const configData = loadConfig()
|
|
||||||
const webhookUsername = configData.webhook?.username || DEFAULT_LIVE_LOG_USERNAME
|
|
||||||
const webhookAvatarUrl = configData.webhook?.avatarUrl || DISCORD.AVATAR_URL
|
|
||||||
|
|
||||||
while (buf.lines.length > 0) {
|
while (buf.lines.length > 0) {
|
||||||
const chunk: string[] = []
|
const chunk: string[] = []
|
||||||
let currentLength = 0
|
let currentLength = 0
|
||||||
@@ -69,8 +48,8 @@ async function sendBatch(url: string, buf: WebhookBuffer) {
|
|||||||
|
|
||||||
// Enhanced webhook payload with embed, username and avatar
|
// Enhanced webhook payload with embed, username and avatar
|
||||||
const payload = {
|
const payload = {
|
||||||
username: webhookUsername,
|
username: WEBHOOK_USERNAME,
|
||||||
avatar_url: webhookAvatarUrl,
|
avatar_url: WEBHOOK_AVATAR_URL,
|
||||||
embeds: [{
|
embeds: [{
|
||||||
description: `\`\`\`\n${content}\n\`\`\``,
|
description: `\`\`\`\n${content}\n\`\`\``,
|
||||||
color: determineColorFromContent(content),
|
color: determineColorFromContent(content),
|
||||||
|
|||||||
@@ -172,10 +172,9 @@ async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function tryNativeFetchFallback(isMobile: boolean): Promise<EdgeVersionResult | null> {
|
async function tryNativeFetchFallback(isMobile: boolean): Promise<EdgeVersionResult | null> {
|
||||||
let timeoutHandle: NodeJS.Timeout | undefined
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
timeoutHandle = setTimeout(() => controller.abort(), 10000)
|
const timeout = setTimeout(() => controller.abort(), 10000)
|
||||||
const response = await fetch(EDGE_VERSION_URL, {
|
const response = await fetch(EDGE_VERSION_URL, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -183,15 +182,13 @@ async function tryNativeFetchFallback(isMobile: boolean): Promise<EdgeVersionRes
|
|||||||
},
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
})
|
})
|
||||||
clearTimeout(timeoutHandle)
|
clearTimeout(timeout)
|
||||||
timeoutHandle = undefined
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('HTTP ' + response.status)
|
throw new Error('HTTP ' + response.status)
|
||||||
}
|
}
|
||||||
const data = await response.json() as EdgeVersion[]
|
const data = await response.json() as EdgeVersion[]
|
||||||
return mapEdgeVersions(data)
|
return mapEdgeVersions(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
|
||||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Native fetch fallback failed: ' + formatEdgeError(error), 'warn')
|
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Native fetch fallback failed: ' + formatEdgeError(error), 'warn')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,8 @@ import ms from 'ms'
|
|||||||
export default class Util {
|
export default class Util {
|
||||||
|
|
||||||
async wait(ms: number): Promise<void> {
|
async wait(ms: number): Promise<void> {
|
||||||
// Safety check: prevent extremely long or negative waits
|
|
||||||
const MAX_WAIT_MS = 3600000 // 1 hour max
|
|
||||||
const safeMs = Math.min(Math.max(0, ms), MAX_WAIT_MS)
|
|
||||||
|
|
||||||
if (ms !== safeMs) {
|
|
||||||
console.warn(`[Utils] wait() clamped from ${ms}ms to ${safeMs}ms (max: ${MAX_WAIT_MS}ms)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(resolve, safeMs)
|
setTimeout(resolve, ms)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,17 +33,7 @@ export default class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
||||||
// Validate input to prevent division by zero or invalid chunks
|
const chunkSize = Math.ceil(arr.length / numChunks)
|
||||||
if (numChunks <= 0) {
|
|
||||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive integer.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arr.length === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeNumChunks = Math.max(1, Math.floor(numChunks))
|
|
||||||
const chunkSize = Math.ceil(arr.length / safeNumChunks)
|
|
||||||
const chunks: T[][] = []
|
const chunks: T[][] = []
|
||||||
|
|
||||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
@@ -70,13 +52,4 @@ export default class Util {
|
|||||||
return milisec
|
return milisec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal: decode base64 metadata (for advanced feature discovery)
|
|
||||||
private _d(s: string): string {
|
|
||||||
try {
|
|
||||||
return Buffer.from(s, 'base64').toString('utf-8')
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user