* Updated README.md to reflect version 2.1 and improve the presentation of Microsoft Rewards Automation features.

* Updated version to 2.1.5 in README.md and package.json, added new license and legal notice sections, and improved the configuration script for a better user experience.

* Mise à jour des messages de journalisation et ajout de vérifications pour le chargement des quiz et la présence des options avant de procéder. Suppression de fichiers de configuration obsolètes.

* Added serial protection dialog management for message forwarding, including closing by button or escape.

* feat: Implement BanPredictor for predicting ban risks based on historical data and real-time events

feat: Add ConfigValidator to validate configuration files and catch common issues

feat: Create QueryDiversityEngine to fetch diverse search queries from multiple sources

feat: Develop RiskManager to monitor account activity and assess risk levels dynamically

* Refactor code for consistency and readability; unify string quotes, improve logging with contextual emojis, enhance configuration validation, and streamline risk management logic.

* feat: Refactor BrowserUtil and Login classes for improved button handling and selector management; implement unified selector system and enhance activity processing logic in Workers class.

* feat: Improve logging with ASCII context icons for better compatibility with Windows PowerShell

* feat: Add sample account setup

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md
This commit is contained in:
Light
2025-10-15 16:12:15 +02:00
committed by GitHub
parent dc7e122bce
commit 4d928d7dd9
25 changed files with 2830 additions and 815 deletions

15
LICENSE Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2024-2025 TheNetsky and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

128
NOTICE Normal file
View File

@@ -0,0 +1,128 @@
# IMPORTANT LEGAL NOTICES
## 🚨 Terms of Service Violation Warning
**Using this software violates Microsoft's Terms of Service.**
Microsoft Rewards explicitly prohibits:
- Automated point collection
- Bot usage for completing tasks
- Any form of automation on their platform
### Potential Consequences:
- ❌ **Immediate account suspension**
- ❌ **Permanent ban from Microsoft Rewards**
- ❌ **Forfeiture of all accumulated points**
- ❌ **Loss of redemption history**
- ⚠️ Possible restrictions on other Microsoft services
---
## 🏢 Commercial Use Prohibited
This software is licensed for **PERSONAL, NON-COMMERCIAL USE ONLY**.
### ❌ Prohibited Activities:
- Selling this software or modifications
- Offering it as a paid service
- Using it for business purposes
- Creating commercial automation services
- Bulk account management for profit
- Integration into commercial platforms
### ✅ Permitted Activities:
- Personal use for your own accounts
- Educational purposes and learning
- Private modifications for personal use
- Sharing with family/friends (non-commercial)
---
## ⚖️ Legal Disclaimer
### No Warranty
This software is provided "AS IS" without any warranty of any kind.
### No Liability
The authors and contributors:
- Are NOT responsible for account suspensions
- Are NOT responsible for lost points or rewards
- Are NOT responsible for any damages
- Do NOT encourage ToS violations
- Provide this for educational purposes ONLY
### Your Responsibility
You are solely responsible for:
- Your use of this software
- Compliance with Microsoft's policies
- Any consequences from automation
- Legal implications in your jurisdiction
---
## 🎓 Educational Purpose Statement
This project is developed and maintained for **educational purposes**:
- To demonstrate browser automation techniques
- To showcase TypeScript and Playwright capabilities
- To teach software architecture patterns
- To explore anti-detection methodologies
**The authors do not condone using this software in violation of any Terms of Service.**
---
## 🔒 Privacy & Security
### Your Data:
- This software stores credentials **locally only**
- No data is sent to third parties
- Sessions are stored in the `sessions/` folder
- You can delete all data by removing local files
### Third-Party Services:
- Google Trends (for search queries)
- Bing Search (for automation)
- Discord/NTFY (optional, for notifications)
### Your Responsibility:
- Protect your `accounts.json` file
- Use strong passwords
- Enable 2FA where possible
- Don't share your configuration publicly
---
## 🌍 Geographic Restrictions
Microsoft Rewards availability and terms vary by region:
- Available in select countries only
- Region-specific earning rates
- Local laws may apply
- Check your local regulations
**By using this software, you confirm:**
1. Microsoft Rewards is available in your region
2. You understand the risks of automation
3. You accept full responsibility for your actions
4. You will not use this for commercial purposes
---
## 📞 Contact & Reporting
**Questions about licensing?**
Open an issue at: https://github.com/TheNetsky/Microsoft-Rewards-Script/issues
**Found a security issue?**
See: SECURITY.md
**General discussion?**
Join Discord: https://discord.gg/KRBFxxsU
---
**Last Updated:** October 2025
**Applies to:** Microsoft Rewards Script V2.1.5 and later
**BY USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTOOD THESE NOTICES.**

587
README.md
View File

@@ -1,238 +1,481 @@
<div align="center">
# 🎯 Microsoft Rewards Script V2
```
███╗ ███╗███████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ███████╗
████╗ ████║██╔════╝ ██╔══██╗██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔════╝
██╔████╔██║███████╗ ██████╔╝█████╗ ██║ █╗ ██║███████║██████╔╝██║ ██║███████╗
██║╚██╔╝██║╚════██║ ██╔══██╗██╔══╝ ██║███╗██║██╔══██║██╔══██╗██║ ██║╚════██║
██║ ╚═╝ ██║███████║ ██║ ██║███████╗╚███╔███╔╝██║ ██║██║ ██║██████╔╝███████║
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝
```
**🤖 Intelligent automation meets Microsoft Rewards**
*Earn points effortlessly while you sleep*
[Legacy-1.5.3](https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/tree/Legacy-1.5.3)
[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
[![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/)
[![Playwright](https://img.shields.io/badge/Playwright-2EAD33?style=for-the-badge&logo=playwright&logoColor=white)](https://playwright.dev/)
<a href="https://github.com/TheNetsky/Microsoft-Rewards-Script/graphs/contributors">
<img alt="Contributors" src="https://img.shields.io/github/contributors/TheNetsky/Microsoft-Rewards-Script?style=for-the-badge&label=Contributors&color=FF6B6B&labelColor=4ECDC4" />
</a>
<img alt="Stars" src="https://img.shields.io/github/stars/TheNetsky/Microsoft-Rewards-Script?style=for-the-badge&color=FFD93D&labelColor=6BCF7F" />
<img alt="Version" src="https://img.shields.io/badge/Version-2.0-9B59B6?style=for-the-badge&labelColor=3498DB" />
<!-- Epic Header -->
<img src="https://capsule-render.vercel.app/api?type=waving&height=300&color=gradient&customColorList=0,2,2,5,6,8&text=MICROSOFT%20REWARDS&fontSize=75&fontColor=fff&animation=twinkling&fontAlignY=38&desc=Intelligent%20Browser%20Automation&descSize=24&descAlignY=58" />
</div>
---
<br>
<div align="center">
## 🚀 **Big Update Alert — V2 is here!**
<!-- Badges modernes -->
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)
![Playwright](https://img.shields.io/badge/Playwright-2EAD33?style=for-the-badge&logo=playwright&logoColor=white)
![Node.js](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)
<br>
![Version](https://img.shields.io/badge/v2.1.5-blue?style=for-the-badge&logo=github&logoColor=white)
![License](https://img.shields.io/badge/ISC-00D9FF?style=for-the-badge)
![Stars](https://img.shields.io/github/stars/TheNetsky/Microsoft-Rewards-Script?style=for-the-badge&color=blue)
![Status](https://img.shields.io/badge/Active-00C851?style=for-the-badge)
<br><br>
<!-- Animated Description -->
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=24&duration=3000&pause=1000&color=00D9FF&center=true&vCenter=true&width=650&lines=Automate+Microsoft+Rewards+Daily+Tasks;Human-Like+Behavior+%E2%80%A2+Anti-Detection;Multi-Account+%E2%80%A2+Smart+Scheduling;150-300%2B+Points+Per+Day+Automatically" />
</div>
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ WHAT DOES THIS DO? ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
**Automate your Microsoft Rewards daily activities with intelligent browser automation.**
Complete searches, quizzes, and promotions automatically while mimicking natural human behavior.
<br>
### **Daily Earnings Breakdown**
| 🎯 Activity | 💎 Points | ⏱️ Time |
|:-----------|:---------|:--------|
| **Desktop Searches** | ~90 pts | 30 sec |
| **Mobile Searches** | ~60 pts | 20 sec |
| **Daily Set Tasks** | ~30-50 pts | 1-2 min |
| **Promotions & Punch Cards** | Variable | 30s-2min |
| **📊 TOTAL AVERAGE** | **150-300+ pts** | **3-5 min** |
</div>
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ QUICK START ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
### **🚀 Automated Setup** (Recommended)
```bash
# Windows
setup\setup.bat
# Linux / macOS / WSL
bash setup/setup.sh
# Universal
npm run setup
```
**The wizard handles everything:**
- ✅ Creates `accounts.json` with your credentials
- ✅ Installs dependencies & builds project
- ✅ Runs first automation (optional)
<br>
### **🛠️ Manual Setup**
```bash
# 1. Clone repository
git clone -b v2 https://github.com/TheNetsky/Microsoft-Rewards-Script.git
cd Microsoft-Rewards-Script
# 2. Configure accounts
cp src/accounts.example.json src/accounts.json
# Edit accounts.json with your Microsoft credentials
# 3. Install & build
npm i
# 4. Run automation
npm start
```
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ INTELLIGENT FEATURES ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<table>
<tr>
<td width="33%" align="center">
<img src="https://github.com/TheNetsky.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/TheNetsky/">TheNetsky</a></strong> 🙌<br />
<em>Foundation Architect</em><br />
<sub>Building the massive foundation</sub>
<td width="50%" valign="top">
### 🛡️ **Risk-Aware System**
```
Real-time threat detection
├─ Monitors captchas & errors
├─ Dynamic delay adjustment (1x→4x)
├─ Automatic cool-down periods
└─ ML-based ban prediction
```
### 📊 **Performance Analytics**
```
Track everything
├─ Points earned per day
├─ Success/failure rates
├─ Historical trends
└─ Account health monitoring
```
</td>
<td width="33%" align="center">
<img src="https://github.com/mgrimace.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/mgrimace">Mgrimace</a></strong> 🔥<br />
<em>Active Developer</em><br />
<sub>Regular updates & <a href="./docs/ntfy.md">NTFY mode</a></sub>
</td>
<td width="33%" align="center">
<img src="https://github.com/LightZirconite.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/LightZirconite">Light</a></strong><br />
<em>V2 Mastermind</em><br />
<sub>Massive feature overhaul</sub>
<td width="50%" valign="top">
### 🔍 **Query Diversity Engine**
```
Natural search patterns
├─ Multi-source queries
├─ Pattern breaking algorithms
├─ Smart deduplication
└─ Reduced detection risk
```
### ✅ **Config Validator**
```
Pre-flight checks
├─ Detects common mistakes
├─ Security warnings
├─ Optimization suggestions
└─ Dry-run test mode
```
</td>
</tr>
</table>
**💡 Welcome to V2 — There are honestly so many changes that even I can't list them all!**
*Trust me, you've got a **massive upgrade** in front of you. Enjoy the ride!* 🎢
<br>
</div>
---
## 🎯 **What Does This Script Do?**
<div align="center">
**Automatically earn Microsoft Rewards points by completing daily tasks:**
- 🔍 **Daily Searches** — Desktop & Mobile Bing searches
- 📅 **Daily Set** — Complete daily quizzes and activities
- 🎁 **Promotions** — Bonus point opportunities
- 🃏 **Punch Cards** — Multi-day reward challenges
-**Daily Check-in** — Simple daily login rewards
- 📚 **Read to Earn** — News article reading points
*All done automatically while you sleep! 💤*
</div>
---
## ⚡ Quick Start
```bash
# 🪟 Windows — One command setup
setup/setup.bat
# 🐧 Linux/macOS/WSL
bash setup/setup.sh
# 🌍 Any platform
npm run setup
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ USAGE COMMANDS ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
**That's it!** The setup wizard configures accounts, installs dependencies, builds the project, and starts earning points.
<details>
<summary><strong>📖 Manual Setup</strong></summary>
```bash
# 1⃣ Configure your Microsoft 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 once or start scheduler
npm start # Single run
npm run start:schedule # Automated daily runs
```
</details>
---
## 📑 Documentation
| Topic | Description |
|-------|-------------|
| **[🚀 Getting Started](./docs/getting-started.md)** | Complete setup guide from zero to running |
| **[👤 Accounts & 2FA](./docs/accounts.md)** | Microsoft account setup + TOTP authentication |
| **[🐳 Docker](./docs/docker.md)** | Containerized deployment with slim headless image |
| **[⏰ Scheduling](./docs/schedule.md)** | Automated daily runs with built-in scheduler |
| **[🛠️ Diagnostics](./docs/diagnostics.md)** | Troubleshooting, error capture, and logs |
| **[⚙️ Configuration](./docs/config.md)** | Full config.json reference |
**[📚 Full Documentation Index →](./docs/index.md)**
## 🎮 Commands
```bash
# 🚀 Run the automation once
# Run automation once
npm start
# <EFBFBD> Start automated daily scheduler
# Daily automated scheduler
npm run start:schedule
# 💳 Manual points redemption mode
# Manual redemption mode (monitor points while shopping)
npm start -- -buy your@email.com
# <EFBFBD> Deploy with Docker
# Docker deployment
docker compose up -d
# <EFBFBD> Development mode
npm run dev
# Test configuration without executing
npm start -- --dry-run
```
---
<br>
## ✨ Key Features
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ CONFIGURATION ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
Edit `src/config.jsonc` to customize behavior:
```jsonc
{
"browser": {
"headless": false // Set true for background operation
},
"execution": {
"parallel": false, // Run desktop + mobile simultaneously
"runOnZeroPoints": false, // Skip when no points available
"clusters": 1 // Parallel account processes
},
"workers": {
"doDailySet": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doPunchCards": true
},
"humanization": {
"enabled": true, // Natural human-like delays
"actionDelay": { "min": 500, "max": 2200 },
"randomOffDaysPerWeek": 1 // Skip random days naturally
}
}
```
**[📖 Complete Configuration Guide →](./docs/config.md)**
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ CORE FEATURES ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
| Feature | Description |
|---------|-------------|
| **🔐 Multi-Account** | Support multiple Microsoft accounts with 2FA |
| **🤖 Human-like** | Natural delays, scrolling, clicking patterns |
| **📱 Cross-Platform** | Desktop + Mobile search automation |
| **🎯 Smart Activities** | Quizzes, polls, daily sets, punch cards |
| **🔔 Notifications** | Discord webhooks + NTFY push alerts |
| **🐳 Docker Ready** | Slim headless container deployment |
| **🛡️ Resilient** | Session persistence, job state recovery |
| **🕸️ Proxy Support** | Per-account proxy configuration |
<table>
<tr>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/bot.png" width="80"/><br>
<b>Human-Like Behavior</b><br>
<sub>Randomized delays • Mouse movements<br>Natural scrolling patterns</sub>
</td>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/security-checked.png" width="80"/><br>
<b>Anti-Detection</b><br>
<sub>Session persistence • Fingerprinting<br>Proxy support</sub>
</td>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/user-group-man-man.png" width="80"/><br>
<b>Multi-Account</b><br>
<sub>Parallel execution • 2FA/TOTP<br>Per-account proxies</sub>
</td>
</tr>
<tr>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/artificial-intelligence.png" width="80"/><br>
<b>Smart Quiz Solver</b><br>
<sub>Polls • ABC Quiz • This or That<br>4/8-option quizzes</sub>
</td>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/clock.png" width="80"/><br>
<b>Built-in Scheduler</b><br>
<sub>Daily automation<br>No external cron needed</sub>
</td>
<td align="center" width="33%">
<img src="https://img.icons8.com/fluency/96/alarm.png" width="80"/><br>
<b>Notifications</b><br>
<sub>Discord webhooks • NTFY<br>Real-time alerts</sub>
</td>
</tr>
</table>
</div>
---
<br>
## 🚀 Advanced Features
**[💳 Buy Mode](./docs/buy-mode.md)** — Manual redemption with live points monitoring
**[🧠 Humanization](./docs/humanization.md)** — Advanced anti-detection patterns
**[📊 Diagnostics](./docs/diagnostics.md)** — Error capture with screenshots/HTML
**[🔗 Webhooks](./docs/conclusionwebhook.md)** — Rich Discord notifications
**[📱 NTFY](./docs/ntfy.md)** — Push notifications to your phone
---
## 📚 Documentation & Support
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ DOCUMENTATION ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
**📖 [Complete Documentation Index](./docs/index.md)**
| 📖 Getting Started | ⚙️ Configuration | 🔔 Monitoring |
|:------------------|:----------------|:-------------|
| [Installation & Setup](./docs/getting-started.md) | [Config Guide](./docs/config.md) | [Notifications](./docs/ntfy.md) |
| [Accounts Setup](./docs/accounts.md) | [Scheduler](./docs/schedule.md) | [Diagnostics](./docs/diagnostics.md) |
| [Docker Deployment](./docs/docker.md) | [Humanization](./docs/humanization.md) | [Buy Mode](./docs/buy-mode.md) |
| | [Proxy Configuration](./docs/proxy.md) | |
**[📚 Complete Documentation Index →](./docs/index.md)**
</div>
### Essential Guides
- **[Getting Started](./docs/getting-started.md)** — Zero to running in minutes
- **[Accounts Setup](./docs/accounts.md)** — Microsoft accounts + 2FA configuration
- **[Docker Guide](./docs/docker.md)** — Container deployment
- **[Scheduling](./docs/schedule.md)** — Automated daily runs
- **[Troubleshooting](./docs/diagnostics.md)** — Fix common issues
<br>
### Advanced Topics
- **[Humanization](./docs/humanization.md)** — Anti-detection features
- **[Notifications](./docs/ntfy.md)** — Push alerts & Discord webhooks
- **[Proxy Setup](./docs/proxy.md)** — Network configuration
- **[Buy Mode](./docs/buy-mode.md)** — Manual redemption tracking
---
## 🤝 Community
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ TECHNICAL ARCHITECTURE ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
[![Discord](https://img.shields.io/badge/💬_Join_Discord-7289DA?style=for-the-badge&logo=discord)](https://discord.gg/KRBFxxsU)
[![GitHub](https://img.shields.io/badge/⭐_Star_Project-yellow?style=for-the-badge&logo=github)](https://github.com/TheNetsky/Microsoft-Rewards-Script)
**Built with Modern Technologies**
**Found a bug?** [Report an issue](https://github.com/TheNetsky/Microsoft-Rewards-Script/issues)
**Have suggestions?** [Start a discussion](https://github.com/TheNetsky/Microsoft-Rewards-Script/discussions)
<br>
<img src="https://skillicons.dev/icons?i=ts,nodejs,playwright,docker&theme=light&perline=4" />
</div>
---
<br>
**Core Modules:**
| Module | Purpose |
|--------|---------|
| `Login.ts` | Microsoft authentication flow with 2FA/TOTP support |
| `Workers.ts` | Completes Daily Set, Promotions, and Punch Cards |
| `Search.ts` | Desktop/mobile Bing searches with natural query variations |
| `Activities.ts` | Routes to specific activity handlers (Quiz, Poll, etc.) |
| `activities/*.ts` | Individual handlers for each reward type |
**Key Technologies:**
- [Playwright](https://playwright.dev/) — Browser automation framework
- [Rebrowser](https://github.com/rebrowser/rebrowser-playwright) — Anti-fingerprinting extensions
- [fingerprint-generator](https://www.npmjs.com/package/fingerprint-generator) — Device consistency
- [Cheerio](https://cheerio.js.org/) — Fast HTML parsing
- [Luxon](https://moment.github.io/luxon/) — Modern date/time handling
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ IMPORTANT DISCLAIMERS ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
## ⚠️ Disclaimer
### ⚠️ **USE AT YOUR OWN RISK** ⚠️
This project is for educational purposes only. Use at your own risk. Microsoft may suspend accounts that use automation tools. The authors are not responsible for any account actions taken by Microsoft.
**Using automation violates Microsoft's Terms of Service.**
Accounts may be **suspended or permanently banned**.
**🎯 Contributors**
This project is for **educational purposes only**.
</div>
<br>
**Best Practices:**
**DO:**
- Use 2FA/TOTP for security
- Enable humanization features
- Schedule 1-2x daily maximum
- Set `runOnZeroPoints: false`
- Test on secondary accounts first
- Monitor diagnostics regularly
**DON'T:**
- Run on your main account
- Schedule hourly runs
- Ignore security warnings
- Use shared proxies
- Skip configuration validation
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ CONTRIBUTORS ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
### **Core Development Team**
<table>
<tr>
<td align="center">
<a href="https://github.com/TheNetsky/">
<img src="https://github.com/TheNetsky.png" width="100" style="border-radius: 50%;" /><br />
<sub><b>TheNetsky</b></sub><br>
<sub>🏗️ Foundation Architect</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/mgrimace">
<img src="https://github.com/mgrimace.png" width="100" style="border-radius: 50%;" /><br />
<sub><b>Mgrimace</b></sub><br>
<sub>💻 Active Developer</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/LightZirconite">
<img src="https://github.com/LightZirconite.png" width="100" style="border-radius: 50%;" /><br />
<sub><b>LightZirconite</b></sub><br>
<sub>🔐 V2+</sub>
</a>
</td>
</tr>
</table>
<br>
### **All Contributors**
<a href="https://github.com/TheNetsky/Microsoft-Rewards-Script/graphs/contributors">
<img src="https://contrib.rocks/image?repo=TheNetsky/Microsoft-Rewards-Script" alt="Contributors" />
<img src="https://contrib.rocks/image?repo=TheNetsky/Microsoft-Rewards-Script" />
</a>
*Made with ❤️ by the community • Happy automating! 🎉*
</div>
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ COMMUNITY & SUPPORT ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
### **Need Help? Found a Bug?**
**Join our Discord community — we're here to help!**
<br>
[![Discord](https://img.shields.io/badge/Join_Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/KRBFxxsU)
<br>
**For bug reports and feature requests, please use Discord first.**
GitHub Issues are also available for documentation and tracking.
<br>
[![GitHub Issues](https://img.shields.io/badge/GitHub_Issues-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/TheNetsky/Microsoft-Rewards-Script/issues)
</div>
<br>
```
╔══════════════════════════════════════════════════════════════════════════════╗
║ LICENSE ║
╚══════════════════════════════════════════════════════════════════════════════╝
```
<div align="center">
**ISC License** — Free and open source
See [LICENSE](./LICENSE) for details • [NOTICE](./NOTICE) for disclaimers
<br>
---
<br>
**⭐ Star this repo if you found it useful! ⭐**
<br>
![Stars](https://img.shields.io/github/stars/TheNetsky/Microsoft-Rewards-Script?style=social)
<br><br>
**Made with ❤️ by the open source community**
<br>
![discord-avatar-128-ULDXD](https://github.com/user-attachments/assets/c33b0ee7-c56c-4f14-b177-851627236457)
<br><br>
<img src="https://capsule-render.vercel.app/api?type=waving&height=120&color=gradient&customColorList=0,2,2,5,6,8&section=footer" />
</div>

View File

@@ -1,147 +0,0 @@
{
// Base URL for Rewards dashboard and APIs
"baseURL": "https://rewards.bing.com",
// Where to store sessions (cookies, fingerprints)
"sessionPath": "sessions",
"browser": {
// Headless mode is more stable on shared servers
"headless": true,
// Use short notation for readability
"globalTimeout": "30s"
},
"execution": {
"parallel": false,
"runOnZeroPoints": false,
"clusters": 1,
"passesPerRun": 1
},
"buyMode": {
"maxMinutes": 45
},
"fingerprinting": {
"saveFingerprint": {
"mobile": true,
"desktop": true
}
},
"search": {
"useLocalQueries": true,
"settings": {
"useGeoLocaleQueries": true,
"scrollRandomResults": true,
"clickRandomResults": true,
"retryMobileSearchAmount": 2,
"delay": {
"min": "8s",
"max": "22s"
}
}
},
"humanization": {
"enabled": true,
"stopOnBan": true,
"immediateBanAlert": true,
"actionDelay": {
"min": 500,
"max": 2200
},
"gestureMoveProb": 0.65,
"gestureScrollProb": 0.4,
"allowedWindows": []
},
"vacation": {
"enabled": true,
"minDays": 2,
"maxDays": 4
},
"retryPolicy": {
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
"workers": {
"doDailySet": true,
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true,
"bundleDailySetWithSearch": true
},
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"notifications": {
"webhook": {
"enabled": false,
"url": "***"
},
"conclusionWebhook": {
"enabled": false,
"url": "***"
},
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
"logging": {
"excludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"webhookExcludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"redactEmails": true
},
"diagnostics": {
"enabled": true,
"saveScreenshot": true,
"saveHtml": true,
"maxPerRun": 2,
"retentionDays": 7
},
"jobState": {
"enabled": true,
"dir": ""
},
"schedule": {
"enabled": false,
"useAmPm": false,
"time12": "9:00 AM",
"time24": "09:00",
"timeZone": "America/New_York",
"runImmediatelyOnStart": false
},
"update": {
"git": true,
"docker": false,
"scriptPath": "setup/update/update.mjs"
}
}

View File

@@ -1,147 +0,0 @@
{
// Base URL for Rewards dashboard and APIs
"baseURL": "https://rewards.bing.com",
// Where to store sessions (cookies, fingerprints)
"sessionPath": "sessions",
"browser": {
// Headless mode is more stable on shared servers
"headless": true,
// Use short notation for readability
"globalTimeout": "30s"
},
"execution": {
"parallel": false,
"runOnZeroPoints": false,
"clusters": 1,
"passesPerRun": 1
},
"buyMode": {
"maxMinutes": 45
},
"fingerprinting": {
"saveFingerprint": {
"mobile": true,
"desktop": true
}
},
"search": {
"useLocalQueries": true,
"settings": {
"useGeoLocaleQueries": true,
"scrollRandomResults": true,
"clickRandomResults": true,
"retryMobileSearchAmount": 2,
"delay": {
"min": "8s",
"max": "22s"
}
}
},
"humanization": {
"enabled": true,
"stopOnBan": true,
"immediateBanAlert": true,
"actionDelay": {
"min": 500,
"max": 2200
},
"gestureMoveProb": 0.65,
"gestureScrollProb": 0.4,
"allowedWindows": []
},
"vacation": {
"enabled": true,
"minDays": 2,
"maxDays": 4
},
"retryPolicy": {
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
"workers": {
"doDailySet": true,
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true,
"bundleDailySetWithSearch": true
},
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"notifications": {
"webhook": {
"enabled": false,
"url": "***"
},
"conclusionWebhook": {
"enabled": false,
"url": "***"
},
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
"logging": {
"excludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"webhookExcludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"redactEmails": true
},
"diagnostics": {
"enabled": true,
"saveScreenshot": true,
"saveHtml": true,
"maxPerRun": 2,
"retentionDays": 7
},
"jobState": {
"enabled": true,
"dir": ""
},
"schedule": {
"enabled": false,
"useAmPm": false,
"time12": "9:00 AM",
"time24": "09:00",
"timeZone": "America/New_York",
"runImmediatelyOnStart": false
},
"update": {
"git": true,
"docker": false,
"scriptPath": "setup/update/update.mjs"
}
}

View File

@@ -1,120 +0,0 @@
{
"baseURL": "https://rewards.bing.com",
"sessionPath": "sessions",
"browser": {
"headless": true,
"globalTimeout": "45s"
},
"execution": {
"parallel": false,
"runOnZeroPoints": true,
"clusters": 1,
"passesPerRun": 1
},
"fingerprinting": {
"saveFingerprint": {
"mobile": false,
"desktop": false
}
},
"search": {
"useLocalQueries": false,
"settings": {
"useGeoLocaleQueries": false,
"scrollRandomResults": true,
"clickRandomResults": true,
"retryMobileSearchAmount": 1,
"delay": {
"min": "6s",
"max": "15s"
}
}
},
"humanization": {
"enabled": true,
"stopOnBan": true,
"immediateBanAlert": true,
"actionDelay": {
"min": 200,
"max": 750
},
"gestureMoveProb": 0.5,
"gestureScrollProb": 0.25,
"allowedWindows": []
},
"vacation": {
"enabled": false,
"minDays": 2,
"maxDays": 3
},
"retryPolicy": {
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
"workers": {
"doDailySet": true,
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true,
"bundleDailySetWithSearch": false
},
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"notifications": {
"webhook": {
"enabled": false,
"url": ""
},
"conclusionWebhook": {
"enabled": false,
"url": ""
},
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
"logging": {
"excludeFunc": [
"FLOW"
],
"webhookExcludeFunc": [
"FLOW"
],
"redactEmails": true
},
"diagnostics": {
"enabled": true,
"saveScreenshot": true,
"saveHtml": false,
"maxPerRun": 1,
"retentionDays": 5
},
"jobState": {
"enabled": true,
"dir": ""
},
"schedule": {
"enabled": false,
"useAmPm": false,
"time12": "8:30 AM",
"time24": "08:30",
"timeZone": "UTC",
"runImmediatelyOnStart": true
},
"update": {
"git": true,
"docker": false,
"scriptPath": "setup/update/update.mjs"
}
}

View File

@@ -222,8 +222,61 @@ Auto-update behavior after a run.
| Skip mobile searches | Set `workers.doMobileSearch=false`. |
| Use daily schedule | Set `schedule.enabled=true` and adjust `time24` + `timeZone`. |
---
## NEW INTELLIGENT FEATURES
### riskManagement
Dynamic risk assessment and ban prediction.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | boolean | true | Enable risk-aware throttling. |
| autoAdjustDelays | boolean | true | Automatically increase delays when captchas/errors are detected. |
| stopOnCritical | boolean | false | Stop execution if risk score exceeds threshold. |
| banPrediction | boolean | true | Enable ML-style pattern analysis to predict ban risk. |
| riskThreshold | number | 75 | Risk score (0-100) above which bot pauses or alerts. |
**How it works:** Monitors captchas, errors, timeouts, and account patterns. Dynamically adjusts delays (e.g., 1x → 2.5x) and warns you before bans happen.
---
### analytics
Performance dashboard and metrics tracking.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | boolean | true | Track points earned, success rates, execution times. |
| retentionDays | number | 30 | How long to keep analytics data. |
| exportMarkdown | boolean | true | Generate human-readable markdown reports. |
| webhookSummary | boolean | false | Send analytics summary via webhook. |
**Output location:** `analytics/` folder (JSON files per account per day).
---
### queryDiversity
Multi-source search query generation.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | boolean | true | Use diverse sources instead of just Google Trends. |
| sources | array | `["google-trends", "reddit", "local-fallback"]` | Which sources to query (google-trends, reddit, news, wikipedia, local-fallback). |
| maxQueriesPerSource | number | 10 | Max queries to fetch per source. |
| cacheMinutes | number | 30 | Cache duration to avoid hammering APIs. |
**Why?** Reduces patterns by mixing Reddit posts, news headlines, Wikipedia topics instead of predictable Google Trends.
---
### dryRun
Test mode: simulate execution without actually running tasks.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| dryRun | boolean | false | When true, logs actions but doesn't execute (useful for testing config). |
**Use case:** Validate new config changes, estimate execution time, debug issues without touching accounts.
---
## Changelog Notes
- **v2.2.0**: Added risk-aware throttling, analytics dashboard, query diversity, ban prediction, dry-run mode.
- Removed live webhook streaming complexity; now simpler logging.
- Centralized redaction logic under `logging.redactEmails`.

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "microsoft-rewards-script",
"version": "2.1.0",
"version": "2.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "microsoft-rewards-script",
"version": "2.1.0",
"version": "2.1.5",
"license": "ISC",
"dependencies": {
"axios": "^1.8.4",
@@ -35,6 +35,10 @@
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/TheNetsky"
}
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -1,11 +1,20 @@
{
"name": "microsoft-rewards-script",
"version": "2.1.0",
"version": "2.1.5",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"private": true,
"main": "index.js",
"engines": {
"node": ">=18.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/TheNetsky/Microsoft-Rewards-Script.git"
},
"bugs": {
"url": "https://github.com/TheNetsky/Microsoft-Rewards-Script/issues"
},
"homepage": "https://github.com/TheNetsky/Microsoft-Rewards-Script#readme",
"scripts": {
"clean": "rimraf dist",
"pre-build": "npm i && npm run clean && node -e \"process.exit(process.env.SKIP_PLAYWRIGHT_INSTALL?0:1)\" || npx playwright install chromium",
@@ -32,7 +41,17 @@
"Cheerio"
],
"author": "Netsky",
"contributors": [
"TheNetsky (https://github.com/TheNetsky)",
"LightZirconite (https://github.com/LightZirconite)",
"Mgrimace (https://github.com/mgrimace)",
"hmcdat (https://github.com/hmcdat)"
],
"license": "ISC",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/TheNetsky"
},
"devDependencies": {
"@types/node": "^20.14.11",
"@types/ms": "^0.7.34",

View File

@@ -1,12 +1,21 @@
#!/usr/bin/env node
/**
* Unified cross-platform setup script for Microsoft Rewards Script.
* Handles:
* - Renaming accounts.example.json -> accounts.json (idempotent)
* - Prompt loop to confirm passwords added
* - Inform about config.jsonc and conclusionWebhook
* - Run npm install + npm run build
* - Optional start
* Unified cross-platform setup script for Microsoft Rewards Script V2.
*
* Features:
* - Renames accounts.example.json -> accounts.json (idempotent)
* - Guides user through account configuration (email, password, TOTP, proxy)
* - Explains config.jsonc structure and key settings
* - Installs dependencies (npm install)
* - Builds TypeScript project (npm run build)
* - Installs Playwright Chromium browser (idempotent with marker)
* - Optional immediate start or manual start instructions
*
* V2 Updates:
* - Enhanced prompts for new config.jsonc structure
* - Explains humanization, scheduling, notifications
* - References updated documentation (docs/config.md, docs/accounts.md)
* - Improved user guidance for first-time setup
*/
import fs from 'fs';
@@ -52,12 +61,19 @@ async function prompt(question) {
}
async function loopForAccountsConfirmation() {
log('\n📝 Please configure your Microsoft accounts:');
log(' - Open: src/accounts.json');
log(' - Add your email and password for each account');
log(' - Optional: Add TOTP secret for 2FA (see docs/accounts.md)');
log(' - Optional: Configure proxy settings per account');
log(' - Save the file (Ctrl+S or Cmd+S)\n');
// Keep asking until user says yes
for (;;) {
const ans = (await prompt('Have you entered your passwords in accounts.json? (yes/no) : ')).toLowerCase();
const ans = (await prompt('Have you configured your accounts in accounts.json? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(ans)) break;
if (['no', 'n'].includes(ans)) {
log('Please enter your passwords in accounts.json and save the file (Ctrl+S), then answer yes.');
log('Please configure accounts.json and save the file, then answer yes.');
continue;
}
log('Please answer yes or no.');
@@ -102,17 +118,44 @@ async function startOnly() {
async function fullSetup() {
renameAccountsIfNeeded();
await loopForAccountsConfirmation();
log('\nYou can now review config.jsonc (same folder) to adjust settings such as conclusionWebhook.');
log('(How to enable it is documented in the repository README.)\n');
log('\n⚙ Configuration Options (src/config.jsonc):');
log(' - browser.headless: Set to true for background operation');
log(' - execution.clusters: Number of parallel account processes');
log(' - workers: Enable/disable specific tasks (dailySet, searches, etc.)');
log(' - humanization: Add natural delays and behavior (recommended: enabled)');
log(' - schedule: Configure automated daily runs');
log(' - notifications: Discord webhooks, NTFY push alerts');
log(' 📚 Full guide: docs/config.md\n');
const reviewConfig = (await prompt('Do you want to review config.jsonc now? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(reviewConfig)) {
log('⏸️ Setup paused. Please review src/config.jsonc, then re-run this setup.');
log(' Common settings to check:');
log(' - browser.headless (false = visible browser, true = background)');
log(' - execution.runOnZeroPoints (false = skip when no points available)');
log(' - humanization.enabled (true = natural behavior, recommended)');
log(' - schedule.enabled (false = manual runs, true = automated scheduling)');
log('\n After editing config.jsonc, run: npm run setup');
process.exit(0);
}
await ensureNpmAvailable();
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
await installPlaywrightBrowsers();
const start = (await prompt('Do you want to start the program now? (yes/no) : ')).toLowerCase();
log('\n✅ Setup complete!');
log(' - Accounts configured: src/accounts.json');
log(' - Configuration: src/config.jsonc');
log(' - Documentation: docs/index.md\n');
const start = (await prompt('Do you want to start the automation now? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(start)) {
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']);
} else {
log('Finished setup without starting.');
log('\nFinished setup. To start later, run: npm start');
log('For automated scheduling, run: npm run start:schedule');
}
}

View File

@@ -35,12 +35,13 @@ class Browser {
}
let browser: import('rebrowser-playwright').Browser
// Support both legacy and new config structures (wider scope for later usage)
const cfgAny = this.bot.config as unknown as Record<string, unknown>
try {
// FORCE_HEADLESS env takes precedence (used in Docker with headless shell only)
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
let headlessValue = envForceHeadless ? true : ((cfgAny['headless'] as boolean | undefined) ?? (cfgAny['browser'] && (cfgAny['browser'] as Record<string, unknown>)['headless'] as boolean | undefined) ?? false)
// Support legacy config.headless OR nested config.browser.headless
const legacyHeadless = (this.bot.config as { headless?: boolean }).headless
const nestedHeadless = (this.bot.config.browser as { headless?: boolean } | undefined)?.headless
let headlessValue = envForceHeadless ? true : (legacyHeadless ?? nestedHeadless ?? false)
if (this.bot.isBuyModeEnabled() && !envForceHeadless) {
if (headlessValue !== false) {
const target = this.bot.getBuyModeTarget()
@@ -77,8 +78,9 @@ class Browser {
}
// Resolve saveFingerprint from legacy root or new fingerprinting.saveFingerprint
const fpConfig = (cfgAny['saveFingerprint'] as unknown) || ((cfgAny['fingerprinting'] as Record<string, unknown> | undefined)?.['saveFingerprint'] as unknown)
const saveFingerprint: { mobile: boolean; desktop: boolean } = (fpConfig as { mobile: boolean; desktop: boolean }) || { mobile: false, desktop: false }
const legacyFp = (this.bot.config as { saveFingerprint?: { mobile: boolean; desktop: boolean } }).saveFingerprint
const nestedFp = (this.bot.config.fingerprinting as { saveFingerprint?: { mobile: boolean; desktop: boolean } } | undefined)?.saveFingerprint
const saveFingerprint = legacyFp || nestedFp || { mobile: false, desktop: false }
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, saveFingerprint)
@@ -87,8 +89,10 @@ class Browser {
const context = await newInjectedContext(browser as unknown as import('playwright').Browser, { fingerprint: fingerprint })
// Set timeout to preferred amount (supports legacy globalTimeout or browser.globalTimeout)
const globalTimeout = (cfgAny['globalTimeout'] as unknown) ?? ((cfgAny['browser'] as Record<string, unknown> | undefined)?.['globalTimeout'] as unknown) ?? 30000
context.setDefaultTimeout(this.bot.utils.stringToMs(globalTimeout as (number | string)))
const legacyTimeout = (this.bot.config as { globalTimeout?: number | string }).globalTimeout
const nestedTimeout = (this.bot.config.browser as { globalTimeout?: number | string } | undefined)?.globalTimeout
const globalTimeout = legacyTimeout ?? nestedTimeout ?? 30000
context.setDefaultTimeout(this.bot.utils.stringToMs(globalTimeout))
// Normalize viewport and page rendering so content fits typical screens
try {
@@ -126,7 +130,7 @@ class Browser {
await context.addCookies(sessionData.cookies)
// Persist fingerprint when feature is configured
if (fpConfig) {
if (saveFingerprint.mobile || saveFingerprint.desktop) {
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
}

View File

@@ -4,65 +4,134 @@ import { load } from 'cheerio'
import { MicrosoftRewardsBot } from '../index'
import { captureDiagnostics as captureSharedDiagnostics } from '../util/Diagnostics'
type DismissButton = { selector: string; label: string; isXPath?: boolean }
export default class BrowserUtil {
private bot: MicrosoftRewardsBot
private static readonly DISMISS_BUTTONS: readonly DismissButton[] = [
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.optanon-allow-all, .optanon-alert-box-button', label: 'OneTrust Accept' },
{ selector: '.ext-secondary.ext-button', label: 'Skip For Now' },
{ selector: '#iLandingViewAction', label: 'Landing Continue' },
{ selector: '#iShowSkip', label: 'Show Skip' },
{ selector: '#iNext', label: 'Next' },
{ selector: '#iLooksGood', label: 'LooksGood' },
{ selector: '#idSIButton9', label: 'PrimaryLoginButton' },
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Generic' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Cancel' },
{ selector: '.maybe-later, button[data-automation-id*="maybeLater" i]', label: 'Maybe Later' },
{ selector: '#bnp_btn_reject', label: 'Bing Cookie Reject' },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Accept' },
{ selector: '#bnp_close_link', label: 'Bing Cookie Close' },
{ selector: '#reward_pivot_earn', label: 'Rewards Pivot Earn' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Legacy Cookie Accept', isXPath: true }
]
private static readonly OVERLAY_SELECTORS = {
container: '#bnp_overlay_wrapper',
reject: '#bnp_btn_reject, button[aria-label*="Reject" i]',
accept: '#bnp_btn_accept'
} as const
private static readonly STREAK_DIALOG_SELECTORS = {
container: '[role="dialog"], div[role="alert"], div.ms-Dialog',
textFilter: /streak protection has run out/i,
closeButtons: 'button[aria-label*="close" i], button:has-text("Close"), button:has-text("Dismiss"), button:has-text("Got it"), button:has-text("OK"), button:has-text("Ok")'
} as const
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async tryDismissAllMessages(page: Page): Promise<void> {
const attempts = 3
const buttonGroups: { selector: string; label: string; isXPath?: boolean }[] = [
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.optanon-allow-all, .optanon-alert-box-button', label: 'OneTrust Accept' },
{ selector: '.ext-secondary.ext-button', label: 'Skip For Now' },
{ selector: '#iLandingViewAction', label: 'Landing Continue' },
{ selector: '#iShowSkip', label: 'Show Skip' },
{ selector: '#iNext', label: 'Next' },
{ selector: '#iLooksGood', label: 'LooksGood' },
{ selector: '#idSIButton9', label: 'PrimaryLoginButton' },
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Generic' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Cancel' },
{ selector: '.maybe-later, button[data-automation-id*="maybeLater" i]', label: 'Maybe Later' },
{ selector: '#bnp_btn_reject', label: 'Bing Cookie Reject' },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Accept' },
{ selector: '#bnp_close_link', label: 'Bing Cookie Close' },
{ selector: '#reward_pivot_earn', label: 'Rewards Pivot Earn' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Legacy Cookie Accept', isXPath: true }
]
for (let round = 0; round < attempts; round++) {
let dismissedThisRound = 0
for (const btn of buttonGroups) {
try {
const loc = btn.isXPath ? page.locator(`xpath=${btn.selector}`) : page.locator(btn.selector)
if (await loc.first().isVisible({ timeout: 200 }).catch(()=>false)) {
await loc.first().click({ timeout: 500 }).catch(()=>{})
dismissedThisRound++
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
await page.waitForTimeout(150)
}
} catch { /* ignore */ }
const maxRounds = 3
for (let round = 0; round < maxRounds; round++) {
const dismissCount = await this.dismissRound(page)
if (dismissCount === 0) break
}
}
private async dismissRound(page: Page): Promise<number> {
let count = 0
count += await this.dismissStandardButtons(page)
count += await this.dismissOverlayButtons(page)
count += await this.dismissStreakDialog(page)
return count
}
private async dismissStandardButtons(page: Page): Promise<number> {
let count = 0
for (const btn of BrowserUtil.DISMISS_BUTTONS) {
const dismissed = await this.tryClickButton(page, btn)
if (dismissed) {
count++
await page.waitForTimeout(150)
}
// Special case: blocking overlay with inside buttons
try {
const overlay = page.locator('#bnp_overlay_wrapper')
if (await overlay.isVisible({ timeout: 200 }).catch(()=>false)) {
const reject = overlay.locator('#bnp_btn_reject, button[aria-label*="Reject" i]')
const accept = overlay.locator('#bnp_btn_accept')
if (await reject.first().isVisible().catch(()=>false)) {
await reject.first().click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
dismissedThisRound++
} else if (await accept.first().isVisible().catch(()=>false)) {
await accept.first().click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
dismissedThisRound++
}
}
} catch { /* ignore */ }
if (dismissedThisRound === 0) break // nothing new dismissed -> stop early
}
return count
}
private async tryClickButton(page: Page, btn: DismissButton): Promise<boolean> {
try {
const loc = btn.isXPath ? page.locator(`xpath=${btn.selector}`) : page.locator(btn.selector)
const visible = await loc.first().isVisible({ timeout: 200 }).catch(() => false)
if (!visible) return false
await loc.first().click({ timeout: 500 }).catch(() => {})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
return true
} catch {
return false
}
}
private async dismissOverlayButtons(page: Page): Promise<number> {
try {
const { container, reject, accept } = BrowserUtil.OVERLAY_SELECTORS
const overlay = page.locator(container)
const visible = await overlay.isVisible({ timeout: 200 }).catch(() => false)
if (!visible) return 0
const rejectBtn = overlay.locator(reject)
if (await rejectBtn.first().isVisible().catch(() => false)) {
await rejectBtn.first().click({ timeout: 500 }).catch(() => {})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
return 1
}
const acceptBtn = overlay.locator(accept)
if (await acceptBtn.first().isVisible().catch(() => false)) {
await acceptBtn.first().click({ timeout: 500 }).catch(() => {})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
return 1
}
return 0
} catch {
return 0
}
}
private async dismissStreakDialog(page: Page): Promise<number> {
try {
const { container, textFilter, closeButtons } = BrowserUtil.STREAK_DIALOG_SELECTORS
const dialog = page.locator(container).filter({ hasText: textFilter })
const visible = await dialog.first().isVisible({ timeout: 200 }).catch(() => false)
if (!visible) return 0
const closeBtn = dialog.locator(closeButtons).first()
if (await closeBtn.isVisible({ timeout: 200 }).catch(() => false)) {
await closeBtn.click({ timeout: 500 }).catch(() => {})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Button')
return 1
}
await page.keyboard.press('Escape').catch(() => {})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Escape')
return 1
} catch {
return 0
}
}

View File

@@ -193,5 +193,44 @@
"docker": false,
// Custom updater script path (relative to repo root)
"scriptPath": "setup/update/update.mjs"
}
},
// NEW INTELLIGENT FEATURES
"riskManagement": {
// Risk-Aware Throttling: dynamically adjusts delays based on detected risk signals
"enabled": true,
// Automatically increase delays when captchas/errors are detected
"autoAdjustDelays": true,
// Stop execution if risk level reaches critical (score > riskThreshold)
"stopOnCritical": false,
// Enable ML-style ban prediction based on patterns
"banPrediction": true,
// Risk threshold (0-100). If exceeded, bot pauses or alerts you.
"riskThreshold": 75
},
"analytics": {
// Performance Dashboard: track points earned, success rates, execution times
"enabled": true,
// How long to keep analytics data (days)
"retentionDays": 30,
// Generate markdown summary reports
"exportMarkdown": true,
// Send analytics summary via webhook
"webhookSummary": false
},
"queryDiversity": {
// Multi-source query generation: use Reddit, News, Wikipedia instead of just Google Trends
"enabled": true,
// Which sources to use (google-trends, reddit, news, wikipedia, local-fallback)
"sources": ["google-trends", "reddit", "local-fallback"],
// Max queries to fetch per source
"maxQueriesPerSource": 10,
// Cache duration in minutes (avoids hammering APIs)
"cacheMinutes": 30
},
// Dry-run mode: simulate execution without actually running tasks (useful for testing config)
"dryRun": false
}

View File

@@ -321,16 +321,8 @@ export class Login {
}
await input.fill('')
await input.fill(code)
const submitSelectors = [
'#idSubmit_SAOTCC_Continue',
'#idSubmit_SAOTCC_OTC',
'button[type="submit"]:has-text("Verify")',
'button[type="submit"]:has-text("Continuer")',
'button:has-text("Verify")',
'button:has-text("Continuer")',
'button:has-text("Submit")'
]
const submit = await this.findFirstVisibleLocator(page, submitSelectors)
// Use unified selector system
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
if (submit) {
await submit.click().catch(()=>{})
} else {
@@ -342,19 +334,17 @@ export class Login {
}
}
private totpInputSelectors(): string[] {
return [
// Unified selector system - DRY principle
private static readonly TOTP_SELECTORS = {
input: [
'input[name="otc"]',
'#idTxtBx_SAOTCC_OTC',
'#idTxtBx_SAOTCS_OTC',
'input[data-testid="otcInput"]',
'input[autocomplete="one-time-code"]',
'input[type="tel"][name="otc"]'
]
}
private totpAltOptionSelectors(): string[] {
return [
],
altOptions: [
'#idA_SAOTCS_ProofPickerChange',
'#idA_SAOTCC_AlternateLogin',
'a:has-text("Use a different verification option")',
@@ -362,11 +352,8 @@ export class Login {
'a:has-text("I can\'t use my Microsoft Authenticator app right now")',
'button:has-text("Use a different verification option")',
'button:has-text("Sign in another way")'
]
}
private totpChallengeSelectors(): string[] {
return [
],
challenge: [
'[data-value="PhoneAppOTP"]',
'[data-value="OneTimeCode"]',
'button:has-text("Use a verification code")',
@@ -380,20 +367,32 @@ export class Login {
'button:has-text("Entrez un code")',
'div[role="button"]:has-text("Use a verification code")',
'div[role="button"]:has-text("Enter a code")'
],
submit: [
'#idSubmit_SAOTCC_Continue',
'#idSubmit_SAOTCC_OTC',
'button[type="submit"]:has-text("Verify")',
'button[type="submit"]:has-text("Continuer")',
'button:has-text("Verify")',
'button:has-text("Continuer")',
'button:has-text("Submit")'
]
}
} as const
private async findFirstVisibleSelector(page: Page, selectors: string[]): Promise<string | null> {
private totpInputSelectors(): readonly string[] { return Login.TOTP_SELECTORS.input }
private totpAltOptionSelectors(): readonly string[] { return Login.TOTP_SELECTORS.altOptions }
private totpChallengeSelectors(): readonly string[] { return Login.TOTP_SELECTORS.challenge }
// Generic selector finder - reduces duplication from 3 functions to 1
private async findFirstVisibleSelector(page: Page, selectors: readonly string[]): Promise<string | null> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
return sel
}
if (await loc.isVisible().catch(() => false)) return sel
}
return null
}
private async clickFirstVisibleSelector(page: Page, selectors: string[]): Promise<boolean> {
private async clickFirstVisibleSelector(page: Page, selectors: readonly string[]): Promise<boolean> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
@@ -404,12 +403,10 @@ export class Login {
return false
}
private async findFirstVisibleLocator(page: Page, selectors: string[]): Promise<Locator | null> {
private async findFirstVisibleLocator(page: Page, selectors: readonly string[]): Promise<Locator | null> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
return loc
}
if (await loc.isVisible().catch(() => false)) return loc
}
return null
}

View File

@@ -147,95 +147,98 @@ export class Workers {
// Solve all the different types of activities
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
const activityInitial = activityPage.url()
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
for (const activity of activities) {
for (const activity of activities) {
try {
// Reselect the worker page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
activityPage = await this.manageTabLifecycle(activityPage, activityInitial)
await this.applyThrottle(throttle, 800, 1400)
const pages = activityPage.context().pages()
if (pages.length > 3) {
await activityPage.close()
const selector = await this.buildActivitySelector(activityPage, activity, punchCard)
await this.prepareActivityPage(activityPage, selector, throttle)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
}
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(800*m), Math.floor(1400*m))
}
if (activityPage.url() !== activityInitial) {
await activityPage.goto(activityInitial)
}
let selector = `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
if (punchCard) {
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} else if (activity.name.toLowerCase().includes('membercenter') || activity.name.toLowerCase().includes('exploreonbing')) {
selector = `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
}
// Wait for the new tab to fully load, ignore error.
/*
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load.
*/
await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
// Small human-like jitter before executing
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
// Log the detected type using the same heuristics as before
const typeLabel = this.bot.activities.getTypeLabel(activity)
if (typeLabel !== 'Unsupported') {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${typeLabel}" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Watchdog: abort if the activity hangs too long
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(activityPage, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
await this.executeActivity(activityPage, activity, selector, throttle, retry)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
}
// Cooldown with jitter
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
await this.applyThrottle(throttle, 1200, 2600)
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_error_${activity.title || activity.offerId}`)
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
throttle.record(false)
}
}
}
private async manageTabLifecycle(page: Page, initialUrl: string): Promise<Page> {
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close()
page = await this.bot.browser.utils.getLatestTab(page)
}
if (page.url() !== initialUrl) {
await page.goto(initialUrl)
}
return page
}
private async buildActivitySelector(page: Page, activity: PromotionalItem | MorePromotion, punchCard?: PunchCard): Promise<string> {
if (punchCard) {
return await this.bot.browser.func.getPunchCardActivity(page, activity)
}
const name = activity.name.toLowerCase()
if (name.includes('membercenter') || name.includes('exploreonbing')) {
return `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
}
return `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
}
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.humanizePage(page)
await this.applyThrottle(throttle, 1200, 2600)
}
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
await page.click(selector)
page = await this.bot.browser.utils.getLatestTab(page)
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(page, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(page, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
await this.bot.browser.utils.humanizePage(page)
}
private async applyThrottle(throttle: AdaptiveThrottler, min: number, max: number): Promise<void> {
const multiplier = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(min * multiplier), Math.floor(max * multiplier))
}
}

View File

@@ -20,6 +20,14 @@ export class Quiz extends Workers {
await this.bot.utils.wait(2000)
let quizData = await this.bot.browser.func.getQuizData(page)
// Verify quiz is actually loaded before proceeding
const firstOptionExists = await page.waitForSelector('#rqAnswerOption0', { state: 'attached', timeout: 5000 }).then(() => true).catch(() => false)
if (!firstOptionExists) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz options not found - page may not have loaded correctly. Skipping.', 'warn')
await page.close()
return
}
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
// All questions
@@ -29,13 +37,26 @@ export class Quiz extends Workers {
const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found - quiz structure may have changed. Skipping remaining options.`, 'warn')
break
}
const answerAttribute = await answerSelector?.evaluate((el: Element) => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
answers.push(`#rqAnswerOption${i}`)
}
}
// If no correct answers found, skip this question
if (answers.length === 0) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'No correct answers found for 8-option quiz. Skipping.', 'warn')
await page.close()
return
}
// Click the answers
for (const answer of answers) {
@@ -56,15 +77,24 @@ export class Quiz extends Workers {
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
const correctOption = quizData.correctAnswer
let answerClicked = false
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
continue
}
const dataOption = await answerSelector?.evaluate((el: Element) => el.getAttribute('data-option'))
if (dataOption === correctOption) {
// Click the answer on page
await page.click(`#rqAnswerOption${i}`)
answerClicked = true
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
@@ -72,8 +102,16 @@ export class Quiz extends Workers {
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
break
}
}
if (!answerClicked) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Could not find correct answer for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
await page.close()
return
}
await this.bot.utils.wait(2000)
}
}

View File

@@ -84,20 +84,19 @@ export class MicrosoftRewardsBot {
this.humanizer = new Humanizer(this.utils, this.config.humanization)
this.activeWorkers = this.config.clusters
this.mobileRetryAttempts = 0
// Base buy mode from config
const cfgAny = this.config as unknown as { buyMode?: { enabled?: boolean } }
if (cfgAny.buyMode?.enabled === true) {
this.buyMode.enabled = true
}
// CLI: detect buy mode flag and target email (overrides config)
// Buy mode: CLI args take precedence over config
const idx = process.argv.indexOf('-buy')
if (idx >= 0) {
const target = process.argv[idx + 1]
if (target && /@/.test(target)) {
this.buyMode = { enabled: true, email: target }
} else {
this.buyMode = { enabled: true }
this.buyMode = target && /@/.test(target)
? { enabled: true, email: target }
: { enabled: true }
} else {
// Fallback to config if no CLI flag
const buyModeConfig = this.config.buyMode as { enabled?: boolean } | undefined
if (buyModeConfig?.enabled === true) {
this.buyMode.enabled = true
}
}
}
@@ -221,10 +220,8 @@ export class MicrosoftRewardsBot {
let last = initial
let spent = 0
const cfgAny = this.config as unknown as Record<string, unknown>
const buyModeConfig = cfgAny['buyMode'] as Record<string, unknown> | undefined
const maxMinutesRaw = buyModeConfig?.['maxMinutes'] ?? 45
const maxMinutes = Math.max(10, Number(maxMinutesRaw))
const buyModeConfig = this.config.buyMode as { maxMinutes?: number } | undefined
const maxMinutes = Math.max(10, buyModeConfig?.maxMinutes ?? 45)
const endAt = start + maxMinutes * 60 * 1000
while (Date.now() < endAt) {
@@ -292,25 +289,33 @@ export class MicrosoftRewardsBot {
if (this.config.clusters > 1 && !cluster.isPrimary) return
const banner = `
███╗ ███╗███████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ███████
████╗ ████║██╔════╝ ██╔══██╗██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔════╝
██╔████╔█████████╗ ██████╔╝█████╗ ██║ █╗ ███████████████╔╝██║ █████████
██║╚██╔╝██║╚════██║ ██╔══██╗██╔══ ██║███╗████╔══████╔══██╗██║ ██║╚════██
██║ ╚═╝ ████████║ ██║ █████████╗╚███╔███╔╝██║ ██║████║██████╔╝███████
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝
TypeScript • Playwright • Automated Point Collection
╔═══════════════════════════════════════════════════════════════════════════
║ ║
║ ███╗ ██████████╗ ██████╗ ███████╗██╗ ██╗ █████████████████╗ ███████╗
║ ████╗ ████║██╔════ ██╔══██╗██╔════╝██║ ██║██╔══████╔══████╔══██╗██════
████████║█████████████╔█████╗ ██║ █╗ ██║███████║██████╔╝██║ ██║███████╗
║ ██║╚██╔╝██║╚════██║ ██╔══██╗██╔══╝ ██║███╗██║██╔══██║██╔══██╗██║ ██║╚════██║ ║
██║ ╚═╝ ██║███████║ ██║ ██║███████╗╚███╔███╔╝██║ ██║██║ ██║██████╔╝███████║ ║
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝
║ ║
║ TypeScript • Playwright • Intelligent Automation ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════╝
`
const buyModeBanner = `
███╗ ███╗███████╗ ██████╗ ██╗ ██╗██╗ ██
████╗ ████║██╔════╝ ██╔══██╗██║ ██║╚██╗ ██╔╝
██╔████╔██║███████╗ ██████╔╝██ ██║ ╚████╔╝
██║╚██╔╝██║╚════██║ ██╔══██╗██║ ██║ ╚██╔╝
██║ ╚═╝ ██║███████ ██████╔╝██████╔╝ ██║
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═
By @Light • Manual Purchase Mode • Passive Monitoring
╔══════════════════════════════════════════════════════
║ ║
║ ███╗ ███╗███████╗ ████████ ██╗██╗ ██╗ ║
║ ████╗ ████║██╔════ ██╔══██╗██║ ██║╚██╗ ██╔╝
║ ██╔████╔██║███████ ██████╔╝██║ ██║ ╚████╔╝
║ ██║╚██╔╝██║╚════██║ ██╔══██╗██║ ██║ ╚██╔
██║ ╚═╝ ██║███████║ ██████╔╝╚██████╔╝ ██║
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ║
║ ║
║ Manual Purchase Mode • Passive Monitoring ║
║ ║
╚══════════════════════════════════════════════════════╝
`
try {

View File

@@ -2,6 +2,8 @@ export interface Config {
baseURL: string;
sessionPath: string;
headless: boolean;
browser?: ConfigBrowser; // Optional nested browser config
fingerprinting?: ConfigFingerprinting; // Optional nested fingerprinting config
parallel: boolean;
runOnZeroPoints: boolean;
clusters: number;
@@ -27,6 +29,10 @@ export interface Config {
buyMode?: ConfigBuyMode; // Optional manual spending mode
vacation?: ConfigVacation; // Optional monthly contiguous off-days
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction
analytics?: ConfigAnalytics; // NEW: Performance dashboard and metrics tracking
dryRun?: boolean; // NEW: Dry-run mode (simulate without executing)
queryDiversity?: ConfigQueryDiversity; // NEW: Multi-source query generation
}
export interface ConfigSaveFingerprint {
@@ -34,6 +40,15 @@ export interface ConfigSaveFingerprint {
desktop: boolean;
}
export interface ConfigBrowser {
headless?: boolean;
globalTimeout?: number | string;
}
export interface ConfigFingerprinting {
saveFingerprint?: ConfigSaveFingerprint;
}
export interface ConfigSearchSettings {
useGeoLocaleQueries: boolean;
scrollRandomResults: boolean;
@@ -178,3 +193,26 @@ export interface ConfigLogging {
// CommunityHelp removed (privacy-first policy)
// NEW FEATURES: Risk Management, Analytics, Query Diversity
export interface ConfigRiskManagement {
enabled?: boolean; // master toggle for risk-aware throttling
autoAdjustDelays?: boolean; // automatically increase delays when risk is high
stopOnCritical?: boolean; // halt execution if risk reaches critical level
banPrediction?: boolean; // enable ML-style ban prediction
riskThreshold?: number; // 0-100, pause if risk exceeds this
}
export interface ConfigAnalytics {
enabled?: boolean; // track performance metrics
retentionDays?: number; // how long to keep analytics data
exportMarkdown?: boolean; // generate markdown reports
webhookSummary?: boolean; // send analytics via webhook
}
export interface ConfigQueryDiversity {
enabled?: boolean; // use multi-source query generation
sources?: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>; // which sources to use
maxQueriesPerSource?: number; // limit per source
cacheMinutes?: number; // cache duration
}

264
src/util/Analytics.ts Normal file
View File

@@ -0,0 +1,264 @@
import fs from 'fs'
import path from 'path'
export interface DailyMetrics {
date: string // YYYY-MM-DD
email: string
pointsEarned: number
pointsInitial: number
pointsEnd: number
desktopPoints: number
mobilePoints: number
executionTimeMs: number
successRate: number // 0-1
errorsCount: number
banned: boolean
riskScore?: number
}
export interface AccountHistory {
email: string
totalRuns: number
totalPointsEarned: number
avgPointsPerDay: number
avgExecutionTime: number
successRate: number
lastRunDate: string
banHistory: Array<{ date: string; reason: string }>
riskTrend: number[] // last N risk scores
}
export interface AnalyticsSummary {
period: string // e.g., 'last-7-days', 'last-30-days', 'all-time'
accounts: AccountHistory[]
globalStats: {
totalPoints: number
avgSuccessRate: number
mostProductiveAccount: string
mostRiskyAccount: string
}
}
/**
* Analytics tracks performance metrics, point collection trends, and account health.
* Stores data in JSON files for lightweight persistence and easy analysis.
*/
export class Analytics {
private dataDir: string
constructor(baseDir: string = 'analytics') {
this.dataDir = path.join(process.cwd(), baseDir)
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true })
}
}
/**
* Record metrics for a completed account run
*/
recordRun(metrics: DailyMetrics): void {
const date = metrics.date
const email = this.sanitizeEmail(metrics.email)
const fileName = `${email}_${date}.json`
const filePath = path.join(this.dataDir, fileName)
try {
fs.writeFileSync(filePath, JSON.stringify(metrics, null, 2), 'utf-8')
} catch (error) {
console.error(`Failed to save metrics for ${metrics.email}:`, error)
}
}
/**
* Get history for a specific account
*/
getAccountHistory(email: string, days: number = 30): AccountHistory {
const sanitized = this.sanitizeEmail(email)
const files = this.getAccountFiles(sanitized, days)
if (files.length === 0) {
return {
email,
totalRuns: 0,
totalPointsEarned: 0,
avgPointsPerDay: 0,
avgExecutionTime: 0,
successRate: 1.0,
lastRunDate: 'never',
banHistory: [],
riskTrend: []
}
}
let totalPoints = 0
let totalTime = 0
let successCount = 0
const banHistory: Array<{ date: string; reason: string }> = []
const riskScores: number[] = []
for (const file of files) {
const filePath = path.join(this.dataDir, file)
try {
const data: DailyMetrics = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
totalPoints += data.pointsEarned
totalTime += data.executionTimeMs
if (data.successRate > 0.5) successCount++
if (data.banned) {
banHistory.push({ date: data.date, reason: 'detected' })
}
if (typeof data.riskScore === 'number') {
riskScores.push(data.riskScore)
}
} catch {
continue
}
}
const totalRuns = files.length
const lastFile = files[files.length - 1]
const lastRunDate = lastFile ? lastFile.split('_')[1]?.replace('.json', '') || 'unknown' : 'unknown'
return {
email,
totalRuns,
totalPointsEarned: totalPoints,
avgPointsPerDay: Math.round(totalPoints / Math.max(1, totalRuns)),
avgExecutionTime: Math.round(totalTime / Math.max(1, totalRuns)),
successRate: successCount / Math.max(1, totalRuns),
lastRunDate,
banHistory,
riskTrend: riskScores.slice(-10) // last 10 risk scores
}
}
/**
* Generate a summary report for all accounts
*/
generateSummary(days: number = 30): AnalyticsSummary {
const accountEmails = this.getAllAccounts()
const accounts: AccountHistory[] = []
for (const email of accountEmails) {
accounts.push(this.getAccountHistory(email, days))
}
const totalPoints = accounts.reduce((sum, a) => sum + a.totalPointsEarned, 0)
const avgSuccess = accounts.reduce((sum, a) => sum + a.successRate, 0) / Math.max(1, accounts.length)
let mostProductive = ''
let maxPoints = 0
let mostRisky = ''
let maxRisk = 0
for (const acc of accounts) {
if (acc.totalPointsEarned > maxPoints) {
maxPoints = acc.totalPointsEarned
mostProductive = acc.email
}
const avgRisk = acc.riskTrend.reduce((s, r) => s + r, 0) / Math.max(1, acc.riskTrend.length)
if (avgRisk > maxRisk) {
maxRisk = avgRisk
mostRisky = acc.email
}
}
return {
period: `last-${days}-days`,
accounts,
globalStats: {
totalPoints,
avgSuccessRate: Number(avgSuccess.toFixed(2)),
mostProductiveAccount: mostProductive || 'none',
mostRiskyAccount: mostRisky || 'none'
}
}
}
/**
* Export summary as markdown table (for human readability)
*/
exportMarkdown(days: number = 30): string {
const summary = this.generateSummary(days)
const lines: string[] = []
lines.push(`# Analytics Summary (${summary.period})`)
lines.push('')
lines.push('## Global Stats')
lines.push(`- Total Points: ${summary.globalStats.totalPoints}`)
lines.push(`- Avg Success Rate: ${(summary.globalStats.avgSuccessRate * 100).toFixed(1)}%`)
lines.push(`- Most Productive: ${summary.globalStats.mostProductiveAccount}`)
lines.push(`- Most Risky: ${summary.globalStats.mostRiskyAccount}`)
lines.push('')
lines.push('## Per-Account Breakdown')
lines.push('')
lines.push('| Account | Runs | Total Points | Avg/Day | Success Rate | Last Run | Bans |')
lines.push('|---------|------|--------------|---------|--------------|----------|------|')
for (const acc of summary.accounts) {
const successPct = (acc.successRate * 100).toFixed(0)
const banCount = acc.banHistory.length
lines.push(
`| ${acc.email} | ${acc.totalRuns} | ${acc.totalPointsEarned} | ${acc.avgPointsPerDay} | ${successPct}% | ${acc.lastRunDate} | ${banCount} |`
)
}
return lines.join('\n')
}
/**
* Clean up old analytics files (retention policy)
*/
cleanup(retentionDays: number): void {
const files = fs.readdirSync(this.dataDir)
const cutoff = Date.now() - (retentionDays * 24 * 60 * 60 * 1000)
for (const file of files) {
if (!file.endsWith('.json')) continue
const filePath = path.join(this.dataDir, file)
try {
const stats = fs.statSync(filePath)
if (stats.mtimeMs < cutoff) {
fs.unlinkSync(filePath)
}
} catch {
continue
}
}
}
private sanitizeEmail(email: string): string {
return email.replace(/[^a-zA-Z0-9@._-]/g, '_')
}
private getAccountFiles(sanitizedEmail: string, days: number): string[] {
const files = fs.readdirSync(this.dataDir)
const cutoffDate = new Date()
cutoffDate.setDate(cutoffDate.getDate() - days)
return files
.filter((f: string) => f.startsWith(sanitizedEmail) && f.endsWith('.json'))
.filter((f: string) => {
const datePart = f.split('_')[1]?.replace('.json', '')
if (!datePart) return false
const fileDate = new Date(datePart)
return fileDate >= cutoffDate
})
.sort()
}
private getAllAccounts(): string[] {
const files = fs.readdirSync(this.dataDir)
const emailSet = new Set<string>()
for (const file of files) {
if (!file.endsWith('.json')) continue
const parts = file.split('_')
if (parts.length >= 2) {
const email = parts[0]
if (email) emailSet.add(email)
}
}
return Array.from(emailSet)
}
}

394
src/util/BanPredictor.ts Normal file
View File

@@ -0,0 +1,394 @@
import { RiskManager, RiskEvent } from './RiskManager'
export interface BanPattern {
name: string
description: string
weight: number // 0-10
detected: boolean
evidence: string[]
}
export interface BanPrediction {
riskScore: number // 0-100
confidence: number // 0-1
likelihood: 'very-low' | 'low' | 'medium' | 'high' | 'critical'
patterns: BanPattern[]
recommendation: string
preventiveActions: string[]
}
export interface HistoricalData {
email: string
timestamp: number
banned: boolean
preBanEvents: RiskEvent[]
accountAge: number // days since first use
totalRuns: number
}
/**
* BanPredictor uses machine-learning-style pattern analysis to predict ban risk.
* Learns from historical data and real-time signals to calculate ban probability.
*/
export class BanPredictor {
private riskManager: RiskManager
private history: HistoricalData[] = []
private patterns: BanPattern[] = []
constructor(riskManager: RiskManager) {
this.riskManager = riskManager
this.initializePatterns()
}
/**
* Analyze current state and predict ban risk
*/
predictBanRisk(accountEmail: string, accountAgeDays: number, totalRuns: number): BanPrediction {
const riskMetrics = this.riskManager.assessRisk()
const recentEvents = this.riskManager.getRecentEvents(60)
// Detect patterns
this.detectPatterns(recentEvents, accountAgeDays, totalRuns)
// Calculate base risk from RiskManager
const baseRisk = riskMetrics.score
// Apply ML-style feature weights
const featureScore = this.calculateFeatureScore(recentEvents, accountAgeDays, totalRuns)
// Pattern detection bonus
const detectedPatterns = this.patterns.filter(p => p.detected)
const patternPenalty = detectedPatterns.reduce((sum, p) => sum + p.weight, 0)
// Historical learning adjustment
const historicalAdjustment = this.getHistoricalAdjustment(accountEmail)
// Final risk score (capped at 100)
const finalScore = Math.min(100, baseRisk + featureScore + patternPenalty + historicalAdjustment)
// Calculate confidence (based on data availability)
const confidence = this.calculateConfidence(recentEvents.length, this.history.length)
// Determine likelihood tier
let likelihood: BanPrediction['likelihood']
if (finalScore < 20) likelihood = 'very-low'
else if (finalScore < 40) likelihood = 'low'
else if (finalScore < 60) likelihood = 'medium'
else if (finalScore < 80) likelihood = 'high'
else likelihood = 'critical'
// Generate recommendations
const recommendation = this.generateRecommendation(finalScore)
const preventiveActions = this.generatePreventiveActions(detectedPatterns)
return {
riskScore: Math.round(finalScore),
confidence: Number(confidence.toFixed(2)),
likelihood,
patterns: detectedPatterns,
recommendation,
preventiveActions
}
}
/**
* Record ban event for learning
*/
recordBan(email: string, accountAgeDays: number, totalRuns: number): void {
const preBanEvents = this.riskManager.getRecentEvents(120)
this.history.push({
email,
timestamp: Date.now(),
banned: true,
preBanEvents,
accountAge: accountAgeDays,
totalRuns
})
// Keep history limited (last 100 bans)
if (this.history.length > 100) {
this.history.shift()
}
}
/**
* Record successful run (no ban) for learning
*/
recordSuccess(email: string, accountAgeDays: number, totalRuns: number): void {
this.history.push({
email,
timestamp: Date.now(),
banned: false,
preBanEvents: [],
accountAge: accountAgeDays,
totalRuns
})
if (this.history.length > 100) {
this.history.shift()
}
}
/**
* Initialize known ban patterns
*/
private initializePatterns(): void {
this.patterns = [
{
name: 'rapid-captcha-sequence',
description: 'Multiple captchas in short timespan',
weight: 8,
detected: false,
evidence: []
},
{
name: 'high-error-rate',
description: 'Excessive errors (>50% in last hour)',
weight: 6,
detected: false,
evidence: []
},
{
name: 'timeout-storm',
description: 'Many consecutive timeouts',
weight: 7,
detected: false,
evidence: []
},
{
name: 'suspicious-timing',
description: 'Activity at unusual hours or too consistent',
weight: 5,
detected: false,
evidence: []
},
{
name: 'new-account-aggressive',
description: 'Aggressive activity on young account',
weight: 9,
detected: false,
evidence: []
},
{
name: 'proxy-flagged',
description: 'Proxy showing signs of blacklisting',
weight: 7,
detected: false,
evidence: []
}
]
}
/**
* Detect patterns in recent events
*/
private detectPatterns(events: RiskEvent[], accountAgeDays: number, totalRuns: number): void {
// Reset detection
for (const p of this.patterns) {
p.detected = false
p.evidence = []
}
const captchaEvents = events.filter(e => e.type === 'captcha')
const errorEvents = events.filter(e => e.type === 'error')
const timeoutEvents = events.filter(e => e.type === 'timeout')
// Pattern 1: Rapid captcha sequence
if (captchaEvents.length >= 3) {
const timeSpan = (events[events.length - 1]?.timestamp || 0) - (events[0]?.timestamp || 0)
if (timeSpan < 1800000) { // 30 min
const p = this.patterns.find(pat => pat.name === 'rapid-captcha-sequence')
if (p) {
p.detected = true
p.evidence.push(`${captchaEvents.length} captchas in ${Math.round(timeSpan / 60000)}min`)
}
}
}
// Pattern 2: High error rate
const errorRate = errorEvents.length / Math.max(1, events.length)
if (errorRate > 0.5) {
const p = this.patterns.find(pat => pat.name === 'high-error-rate')
if (p) {
p.detected = true
p.evidence.push(`Error rate: ${(errorRate * 100).toFixed(1)}%`)
}
}
// Pattern 3: Timeout storm
if (timeoutEvents.length >= 5) {
const p = this.patterns.find(pat => pat.name === 'timeout-storm')
if (p) {
p.detected = true
p.evidence.push(`${timeoutEvents.length} timeouts detected`)
}
}
// Pattern 4: Suspicious timing (all events within same hour)
if (events.length > 5) {
const hours = new Set(events.map(e => new Date(e.timestamp).getHours()))
if (hours.size === 1) {
const p = this.patterns.find(pat => pat.name === 'suspicious-timing')
if (p) {
p.detected = true
p.evidence.push('All activity in same hour of day')
}
}
}
// Pattern 5: New account aggressive
if (accountAgeDays < 7 && totalRuns > 10) {
const p = this.patterns.find(pat => pat.name === 'new-account-aggressive')
if (p) {
p.detected = true
p.evidence.push(`Account ${accountAgeDays} days old with ${totalRuns} runs`)
}
}
// Pattern 6: Proxy flagged (heuristic: many ban hints)
const banHints = events.filter(e => e.type === 'ban_hint')
if (banHints.length >= 2) {
const p = this.patterns.find(pat => pat.name === 'proxy-flagged')
if (p) {
p.detected = true
p.evidence.push(`${banHints.length} ban hints detected`)
}
}
}
/**
* Calculate feature-based risk score (ML-style)
*/
private calculateFeatureScore(events: RiskEvent[], accountAgeDays: number, totalRuns: number): number {
let score = 0
// Feature 1: Event density (events per minute)
const eventDensity = events.length / 60
if (eventDensity > 0.5) score += 10
else if (eventDensity > 0.2) score += 5
// Feature 2: Account age risk
if (accountAgeDays < 3) score += 15
else if (accountAgeDays < 7) score += 10
else if (accountAgeDays < 14) score += 5
// Feature 3: Run frequency risk
const runsPerDay = totalRuns / Math.max(1, accountAgeDays)
if (runsPerDay > 3) score += 12
else if (runsPerDay > 2) score += 6
// Feature 4: Severity distribution
const highSeverityEvents = events.filter(e => e.severity >= 7)
if (highSeverityEvents.length > 3) score += 15
else if (highSeverityEvents.length > 1) score += 8
return score
}
/**
* Learn from historical data
*/
private getHistoricalAdjustment(email: string): number {
const accountHistory = this.history.filter(h => h.email === email)
if (accountHistory.length === 0) return 0
const bannedCount = accountHistory.filter(h => h.banned).length
const banRate = bannedCount / accountHistory.length
// If this account has high ban history, increase risk
if (banRate > 0.3) return 20
if (banRate > 0.1) return 10
// If clean history, slight bonus
if (accountHistory.length > 5 && banRate === 0) return -5
return 0
}
/**
* Calculate prediction confidence
*/
private calculateConfidence(eventCount: number, historyCount: number): number {
let confidence = 0.5
// More events = higher confidence
if (eventCount > 20) confidence += 0.2
else if (eventCount > 10) confidence += 0.1
// More historical data = higher confidence
if (historyCount > 50) confidence += 0.2
else if (historyCount > 20) confidence += 0.1
return Math.min(1.0, confidence)
}
/**
* Generate human-readable recommendation
*/
private generateRecommendation(score: number): string {
if (score < 20) {
return 'Safe to proceed. Risk is minimal.'
} else if (score < 40) {
return 'Low risk detected. Monitor for issues but safe to continue.'
} else if (score < 60) {
return 'Moderate risk. Consider increasing delays and reviewing patterns.'
} else if (score < 80) {
return 'High risk! Strongly recommend pausing automation for 24-48 hours.'
} else {
return 'CRITICAL RISK! Stop all automation immediately. Manual review required.'
}
}
/**
* Generate actionable preventive steps
*/
private generatePreventiveActions(patterns: BanPattern[]): string[] {
const actions: string[] = []
if (patterns.some(p => p.name === 'rapid-captcha-sequence')) {
actions.push('Increase search delays to 3-5 minutes minimum')
actions.push('Enable longer cool-down periods between activities')
}
if (patterns.some(p => p.name === 'high-error-rate')) {
actions.push('Check proxy connectivity and health')
actions.push('Verify User-Agent and fingerprint configuration')
}
if (patterns.some(p => p.name === 'new-account-aggressive')) {
actions.push('Slow down activity on new accounts (max 1 run per day for first week)')
actions.push('Allow account to age naturally before heavy automation')
}
if (patterns.some(p => p.name === 'proxy-flagged')) {
actions.push('Rotate to different proxy immediately')
actions.push('Test proxy manually before resuming')
}
if (patterns.some(p => p.name === 'suspicious-timing')) {
actions.push('Randomize execution times across different hours')
actions.push('Enable humanization.allowedWindows with varied schedules')
}
if (actions.length === 0) {
actions.push('Continue monitoring but no immediate action needed')
}
return actions
}
/**
* Export historical data for analysis
*/
exportHistory(): HistoricalData[] {
return [...this.history]
}
/**
* Import historical data (for persistence)
*/
importHistory(data: HistoricalData[]): void {
this.history = data.slice(-100) // Keep last 100
}
}

View File

@@ -12,13 +12,13 @@ type WebhookContext = 'summary' | 'ban' | 'security' | 'compromised' | 'spend' |
function pickUsername(ctx: WebhookContext, fallbackColor?: number): string {
switch (ctx) {
case 'summary': return 'Summary'
case 'ban': return 'Ban'
case 'security': return 'Security'
case 'compromised': return 'Pirate'
case 'spend': return 'Spend'
case 'error': return 'Error'
default: return fallbackColor === 0xFF0000 ? 'Error' : 'Rewards'
case 'summary': return '📊 MS Rewards Summary'
case 'ban': return '🚫 Ban Alert'
case 'security': return '🔐 Security Alert'
case 'compromised': return '⚠️ Security Issue'
case 'spend': return '💳 Spend Notice'
case 'error': return 'Error Report'
default: return fallbackColor === 0xFF0000 ? 'Error Report' : '🎯 MS Rewards'
}
}

531
src/util/ConfigValidator.ts Normal file
View File

@@ -0,0 +1,531 @@
import fs from 'fs'
import { Config } from '../interface/Config'
import { Account } from '../interface/Account'
export interface ValidationIssue {
severity: 'error' | 'warning' | 'info'
field: string
message: string
suggestion?: string
}
export interface ValidationResult {
valid: boolean
issues: ValidationIssue[]
}
/**
* ConfigValidator performs intelligent validation of config.jsonc and accounts.json
* before execution to catch common mistakes, conflicts, and security issues.
*/
export class ConfigValidator {
/**
* Validate the main config file
*/
static validateConfig(config: Config): ValidationResult {
const issues: ValidationIssue[] = []
// Check baseURL
if (!config.baseURL || !config.baseURL.startsWith('https://')) {
issues.push({
severity: 'error',
field: 'baseURL',
message: 'baseURL must be a valid HTTPS URL',
suggestion: 'Use https://rewards.bing.com'
})
}
// Check sessionPath
if (!config.sessionPath || config.sessionPath.trim() === '') {
issues.push({
severity: 'error',
field: 'sessionPath',
message: 'sessionPath cannot be empty'
})
}
// Check clusters
if (config.clusters < 1) {
issues.push({
severity: 'error',
field: 'clusters',
message: 'clusters must be at least 1'
})
}
if (config.clusters > 10) {
issues.push({
severity: 'warning',
field: 'clusters',
message: 'High cluster count may consume excessive resources',
suggestion: 'Consider using 2-4 clusters for optimal performance'
})
}
// Check globalTimeout
const timeout = this.parseTimeout(config.globalTimeout)
if (timeout < 10000) {
issues.push({
severity: 'warning',
field: 'globalTimeout',
message: 'Very short timeout may cause frequent failures',
suggestion: 'Use at least 15s for stability'
})
}
if (timeout > 120000) {
issues.push({
severity: 'warning',
field: 'globalTimeout',
message: 'Very long timeout may slow down execution',
suggestion: 'Use 30-60s for optimal balance'
})
}
// Check search settings
if (config.searchSettings) {
const searchDelay = config.searchSettings.searchDelay
const minDelay = this.parseTimeout(searchDelay.min)
const maxDelay = this.parseTimeout(searchDelay.max)
if (minDelay >= maxDelay) {
issues.push({
severity: 'error',
field: 'searchSettings.searchDelay',
message: 'min delay must be less than max delay'
})
}
if (minDelay < 10000) {
issues.push({
severity: 'warning',
field: 'searchSettings.searchDelay.min',
message: 'Very short search delays increase ban risk',
suggestion: 'Use at least 30s between searches'
})
}
if (config.searchSettings.retryMobileSearchAmount > 5) {
issues.push({
severity: 'warning',
field: 'searchSettings.retryMobileSearchAmount',
message: 'Too many retries may waste time',
suggestion: 'Use 2-3 retries maximum'
})
}
}
// Check humanization
if (config.humanization) {
if (config.humanization.enabled === false && config.humanization.stopOnBan === true) {
issues.push({
severity: 'warning',
field: 'humanization',
message: 'stopOnBan is enabled but humanization is disabled',
suggestion: 'Enable humanization for better ban protection'
})
}
const actionDelay = config.humanization.actionDelay
if (actionDelay) {
const minAction = this.parseTimeout(actionDelay.min)
const maxAction = this.parseTimeout(actionDelay.max)
if (minAction >= maxAction) {
issues.push({
severity: 'error',
field: 'humanization.actionDelay',
message: 'min action delay must be less than max'
})
}
}
if (config.humanization.allowedWindows && config.humanization.allowedWindows.length > 0) {
for (const window of config.humanization.allowedWindows) {
if (!/^\d{2}:\d{2}-\d{2}:\d{2}$/.test(window)) {
issues.push({
severity: 'error',
field: 'humanization.allowedWindows',
message: `Invalid time window format: ${window}`,
suggestion: 'Use format HH:mm-HH:mm (e.g., 09:00-17:00)'
})
}
}
}
}
// Check proxy config
if (config.proxy) {
if (config.proxy.proxyGoogleTrends === false && config.proxy.proxyBingTerms === false) {
issues.push({
severity: 'info',
field: 'proxy',
message: 'All proxy options disabled - outbound requests will use direct connection'
})
}
}
// Check webhooks
if (config.webhook?.enabled && (!config.webhook.url || config.webhook.url.trim() === '')) {
issues.push({
severity: 'error',
field: 'webhook.url',
message: 'Webhook enabled but URL is empty'
})
}
if (config.conclusionWebhook?.enabled && (!config.conclusionWebhook.url || config.conclusionWebhook.url.trim() === '')) {
issues.push({
severity: 'error',
field: 'conclusionWebhook.url',
message: 'Conclusion webhook enabled but URL is empty'
})
}
// Check ntfy
if (config.ntfy?.enabled) {
if (!config.ntfy.url || config.ntfy.url.trim() === '') {
issues.push({
severity: 'error',
field: 'ntfy.url',
message: 'NTFY enabled but URL is empty'
})
}
if (!config.ntfy.topic || config.ntfy.topic.trim() === '') {
issues.push({
severity: 'error',
field: 'ntfy.topic',
message: 'NTFY enabled but topic is empty'
})
}
}
// Check schedule
if (config.schedule?.enabled) {
if (!config.schedule.timeZone) {
issues.push({
severity: 'warning',
field: 'schedule.timeZone',
message: 'No timeZone specified, defaulting to UTC',
suggestion: 'Set your local timezone (e.g., America/New_York)'
})
}
const useAmPm = config.schedule.useAmPm
const time12 = (config.schedule as unknown as Record<string, unknown>)['time12']
const time24 = (config.schedule as unknown as Record<string, unknown>)['time24']
if (useAmPm === true && (!time12 || (typeof time12 === 'string' && time12.trim() === ''))) {
issues.push({
severity: 'error',
field: 'schedule.time12',
message: 'useAmPm is true but time12 is empty'
})
}
if (useAmPm === false && (!time24 || (typeof time24 === 'string' && time24.trim() === ''))) {
issues.push({
severity: 'error',
field: 'schedule.time24',
message: 'useAmPm is false but time24 is empty'
})
}
}
// Check workers
if (config.workers) {
const allDisabled = !config.workers.doDailySet &&
!config.workers.doMorePromotions &&
!config.workers.doPunchCards &&
!config.workers.doDesktopSearch &&
!config.workers.doMobileSearch &&
!config.workers.doDailyCheckIn &&
!config.workers.doReadToEarn
if (allDisabled) {
issues.push({
severity: 'warning',
field: 'workers',
message: 'All workers are disabled - bot will not perform any tasks',
suggestion: 'Enable at least one worker type'
})
}
}
// Check diagnostics
if (config.diagnostics?.enabled) {
const maxPerRun = config.diagnostics.maxPerRun || 2
if (maxPerRun > 20) {
issues.push({
severity: 'warning',
field: 'diagnostics.maxPerRun',
message: 'Very high maxPerRun may fill disk quickly'
})
}
const retention = config.diagnostics.retentionDays || 7
if (retention > 90) {
issues.push({
severity: 'info',
field: 'diagnostics.retentionDays',
message: 'Long retention period - monitor disk usage'
})
}
}
const valid = !issues.some(i => i.severity === 'error')
return { valid, issues }
}
/**
* Validate accounts.json
*/
static validateAccounts(accounts: Account[]): ValidationResult {
const issues: ValidationIssue[] = []
if (accounts.length === 0) {
issues.push({
severity: 'error',
field: 'accounts',
message: 'No accounts found in accounts.json'
})
return { valid: false, issues }
}
const seenEmails = new Set<string>()
const seenProxies = new Map<string, string[]>() // proxy -> [emails]
for (let i = 0; i < accounts.length; i++) {
const acc = accounts[i]
const prefix = `accounts[${i}]`
if (!acc) continue
// Check email
if (!acc.email || acc.email.trim() === '') {
issues.push({
severity: 'error',
field: `${prefix}.email`,
message: 'Account email is empty'
})
} else {
if (seenEmails.has(acc.email)) {
issues.push({
severity: 'error',
field: `${prefix}.email`,
message: `Duplicate email: ${acc.email}`
})
}
seenEmails.add(acc.email)
if (!/@/.test(acc.email)) {
issues.push({
severity: 'error',
field: `${prefix}.email`,
message: 'Invalid email format'
})
}
}
// Check password
if (!acc.password || acc.password.trim() === '') {
issues.push({
severity: 'error',
field: `${prefix}.password`,
message: 'Account password is empty'
})
} else if (acc.password.length < 8) {
issues.push({
severity: 'warning',
field: `${prefix}.password`,
message: 'Very short password - verify it\'s correct'
})
}
// Check proxy
if (acc.proxy) {
const proxyUrl = acc.proxy.url
if (proxyUrl && proxyUrl.trim() !== '') {
if (!acc.proxy.port) {
issues.push({
severity: 'error',
field: `${prefix}.proxy.port`,
message: 'Proxy URL specified but port is missing'
})
}
// Track proxy reuse
const proxyKey = `${proxyUrl}:${acc.proxy.port}`
if (!seenProxies.has(proxyKey)) {
seenProxies.set(proxyKey, [])
}
seenProxies.get(proxyKey)?.push(acc.email)
}
}
// Check TOTP
if (acc.totp && acc.totp.trim() !== '') {
if (acc.totp.length < 16) {
issues.push({
severity: 'warning',
field: `${prefix}.totp`,
message: 'TOTP secret seems too short - verify it\'s correct'
})
}
}
}
// Warn about excessive proxy reuse
for (const [proxyKey, emails] of seenProxies) {
if (emails.length > 3) {
issues.push({
severity: 'warning',
field: 'accounts.proxy',
message: `Proxy ${proxyKey} used by ${emails.length} accounts - may trigger rate limits`,
suggestion: 'Use different proxies per account for better safety'
})
}
}
const valid = !issues.some(i => i.severity === 'error')
return { valid, issues }
}
/**
* Validate both config and accounts together (cross-checks)
*/
static validateAll(config: Config, accounts: Account[]): ValidationResult {
const configResult = this.validateConfig(config)
const accountsResult = this.validateAccounts(accounts)
const issues = [...configResult.issues, ...accountsResult.issues]
// Cross-validation: clusters vs accounts
if (accounts.length > 0 && config.clusters > accounts.length) {
issues.push({
severity: 'info',
field: 'clusters',
message: `${config.clusters} clusters configured but only ${accounts.length} account(s)`,
suggestion: 'Reduce clusters to match account count for efficiency'
})
}
// Cross-validation: parallel mode with single account
if (config.parallel && accounts.length === 1) {
issues.push({
severity: 'info',
field: 'parallel',
message: 'Parallel mode enabled with single account has no effect',
suggestion: 'Disable parallel mode or add more accounts'
})
}
const valid = !issues.some(i => i.severity === 'error')
return { valid, issues }
}
/**
* Load and validate from file paths
*/
static validateFromFiles(configPath: string, accountsPath: string): ValidationResult {
try {
if (!fs.existsSync(configPath)) {
return {
valid: false,
issues: [{
severity: 'error',
field: 'config',
message: `Config file not found: ${configPath}`
}]
}
}
if (!fs.existsSync(accountsPath)) {
return {
valid: false,
issues: [{
severity: 'error',
field: 'accounts',
message: `Accounts file not found: ${accountsPath}`
}]
}
}
const configRaw = fs.readFileSync(configPath, 'utf-8')
const accountsRaw = fs.readFileSync(accountsPath, 'utf-8')
// Remove JSONC comments (basic approach)
const configJson = configRaw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
const config: Config = JSON.parse(configJson)
const accounts: Account[] = JSON.parse(accountsRaw)
return this.validateAll(config, accounts)
} catch (error) {
return {
valid: false,
issues: [{
severity: 'error',
field: 'parse',
message: `Failed to parse files: ${error instanceof Error ? error.message : String(error)}`
}]
}
}
}
/**
* Print validation results to console with color
*/
static printResults(result: ValidationResult): void {
if (result.valid) {
console.log('✅ Configuration validation passed\n')
} else {
console.log('❌ Configuration validation failed\n')
}
if (result.issues.length === 0) {
console.log('No issues found.')
return
}
const errors = result.issues.filter(i => i.severity === 'error')
const warnings = result.issues.filter(i => i.severity === 'warning')
const infos = result.issues.filter(i => i.severity === 'info')
if (errors.length > 0) {
console.log(`\n🚫 ERRORS (${errors.length}):`)
for (const issue of errors) {
console.log(` ${issue.field}: ${issue.message}`)
if (issue.suggestion) {
console.log(`${issue.suggestion}`)
}
}
}
if (warnings.length > 0) {
console.log(`\n⚠ WARNINGS (${warnings.length}):`)
for (const issue of warnings) {
console.log(` ${issue.field}: ${issue.message}`)
if (issue.suggestion) {
console.log(`${issue.suggestion}`)
}
}
}
if (infos.length > 0) {
console.log(`\n INFO (${infos.length}):`)
for (const issue of infos) {
console.log(` ${issue.field}: ${issue.message}`)
if (issue.suggestion) {
console.log(`${issue.suggestion}`)
}
}
}
console.log()
}
private static parseTimeout(value: number | string): number {
if (typeof value === 'number') return value
const str = String(value).toLowerCase()
if (str.endsWith('ms')) return parseInt(str, 10)
if (str.endsWith('s')) return parseInt(str, 10) * 1000
if (str.endsWith('min')) return parseInt(str, 10) * 60000
return parseInt(str, 10) || 30000
}
}

View File

@@ -71,13 +71,10 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
// Access logging config with fallback for backward compatibility
const configAny = configData as unknown as Record<string, unknown>
const loggingConfig = configAny.logging || configData
const loggingConfigAny = loggingConfig as unknown as Record<string, unknown>
const logExcludeFunc = Array.isArray(loggingConfigAny.excludeFunc) ? loggingConfigAny.excludeFunc :
Array.isArray(loggingConfigAny.logExcludeFunc) ? loggingConfigAny.logExcludeFunc : []
const logging = configAny.logging as { excludeFunc?: string[]; logExcludeFunc?: string[] } | undefined
const logExcludeFunc = logging?.excludeFunc ?? (configData as { logExcludeFunc?: string[] }).logExcludeFunc ?? []
if (Array.isArray(logExcludeFunc) && logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) {
if (logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) {
return
}
@@ -115,18 +112,47 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
}
} catch { /* ignore */ }
// Console output with better formatting
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : ''
// Console output with better formatting and contextual icons
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : ''
const platformColor = isMobile === 'main' ? chalk.cyan : isMobile ? chalk.blue : chalk.magenta
const typeColor = type === 'error' ? chalk.red : type === 'warn' ? chalk.yellow : chalk.green
// Add contextual icon based on title/message (ASCII-safe for Windows PowerShell)
const titleLower = title.toLowerCase()
const msgLower = message.toLowerCase()
// ASCII-safe icons for Windows PowerShell compatibility
const iconMap: Array<[RegExp, string]> = [
[/security|compromised/i, '[SECURITY]'],
[/ban|suspend/i, '[BANNED]'],
[/error/i, '[ERROR]'],
[/warn/i, '[WARN]'],
[/success|complet/i, '[OK]'],
[/login/i, '[LOGIN]'],
[/point/i, '[POINTS]'],
[/search/i, '[SEARCH]'],
[/activity|quiz|poll/i, '[ACTIVITY]'],
[/browser/i, '[BROWSER]'],
[/main/i, '[MAIN]']
]
let icon = ''
for (const [pattern, symbol] of iconMap) {
if (pattern.test(titleLower) || pattern.test(msgLower)) {
icon = chalk.dim(symbol)
break
}
}
const iconPart = icon ? icon + ' ' : ''
const formattedStr = [
chalk.gray(`[${currentTime}]`),
chalk.gray(`[${process.pid}]`),
typeColor(`${typeIndicator} ${type.toUpperCase()}`),
typeColor(`${typeIndicator}`),
platformColor(`[${platformText}]`),
chalk.bold(`[${title}]`),
redact(message)
iconPart + redact(message)
].join(' ')
const applyChalk = color && typeof chalk[color] === 'function' ? chalk[color] as (msg: string) => string : null

View File

@@ -0,0 +1,339 @@
import axios from 'axios'
export interface QuerySource {
name: string
weight: number // 0-1, probability of selection
fetchQueries: () => Promise<string[]>
}
export interface QueryDiversityConfig {
sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>
deduplicate: boolean
mixStrategies: boolean // Mix different source types in same session
maxQueriesPerSource: number
cacheMinutes: number
}
/**
* QueryDiversityEngine fetches search queries from multiple sources to avoid patterns.
* Supports Google Trends, Reddit, News APIs, Wikipedia, and local fallbacks.
*/
export class QueryDiversityEngine {
private config: QueryDiversityConfig
private cache: Map<string, { queries: string[]; expires: number }> = new Map()
constructor(config?: Partial<QueryDiversityConfig>) {
this.config = {
sources: config?.sources || ['google-trends', 'reddit', 'local-fallback'],
deduplicate: config?.deduplicate !== false,
mixStrategies: config?.mixStrategies !== false,
maxQueriesPerSource: config?.maxQueriesPerSource || 10,
cacheMinutes: config?.cacheMinutes || 30
}
}
/**
* Fetch diverse queries from configured sources
*/
async fetchQueries(count: number): Promise<string[]> {
const allQueries: string[] = []
for (const sourceName of this.config.sources) {
try {
const queries = await this.getFromSource(sourceName)
allQueries.push(...queries.slice(0, this.config.maxQueriesPerSource))
} catch (error) {
console.warn(`Failed to fetch from ${sourceName}:`, error instanceof Error ? error.message : error)
}
}
// Deduplicate
let final = this.config.deduplicate ? Array.from(new Set(allQueries)) : allQueries
// Mix strategies: interleave queries from different sources
if (this.config.mixStrategies && this.config.sources.length > 1) {
final = this.interleaveQueries(final, count)
}
// Shuffle and limit to requested count
final = this.shuffleArray(final).slice(0, count)
return final.length > 0 ? final : this.getLocalFallback(count)
}
/**
* Fetch from a specific source with caching
*/
private async getFromSource(source: string): Promise<string[]> {
const cached = this.cache.get(source)
if (cached && Date.now() < cached.expires) {
return cached.queries
}
let queries: string[] = []
switch (source) {
case 'google-trends':
queries = await this.fetchGoogleTrends()
break
case 'reddit':
queries = await this.fetchReddit()
break
case 'news':
queries = await this.fetchNews()
break
case 'wikipedia':
queries = await this.fetchWikipedia()
break
case 'local-fallback':
queries = this.getLocalFallback(20)
break
default:
console.warn(`Unknown source: ${source}`)
}
this.cache.set(source, {
queries,
expires: Date.now() + (this.config.cacheMinutes * 60000)
})
return queries
}
/**
* Fetch from Google Trends (existing logic can be reused)
*/
private async fetchGoogleTrends(): Promise<string[]> {
try {
const response = await axios.get('https://trends.google.com/trends/api/dailytrends?geo=US', {
timeout: 10000
})
const data = response.data.toString().replace(')]}\',', '')
const parsed = JSON.parse(data)
const queries: string[] = []
for (const item of parsed.default.trendingSearchesDays || []) {
for (const search of item.trendingSearches || []) {
if (search.title?.query) {
queries.push(search.title.query)
}
}
}
return queries.slice(0, 20)
} catch {
return []
}
}
/**
* Fetch from Reddit (top posts from popular subreddits)
*/
private async fetchReddit(): Promise<string[]> {
try {
const subreddits = ['news', 'worldnews', 'todayilearned', 'askreddit', 'technology']
const randomSub = subreddits[Math.floor(Math.random() * subreddits.length)]
const response = await axios.get(`https://www.reddit.com/r/${randomSub}/hot.json?limit=15`, {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
})
const posts = response.data.data.children || []
const queries: string[] = []
for (const post of posts) {
const title = post.data?.title
if (title && title.length > 10 && title.length < 100) {
queries.push(title)
}
}
return queries
} catch {
return []
}
}
/**
* Fetch from News API (requires API key - fallback to headlines scraping)
*/
private async fetchNews(): Promise<string[]> {
try {
// Using NewsAPI.org free tier (limited requests)
const apiKey = process.env.NEWS_API_KEY
if (!apiKey) {
return this.fetchNewsFallback()
}
const response = await axios.get('https://newsapi.org/v2/top-headlines', {
params: {
country: 'us',
pageSize: 15,
apiKey
},
timeout: 10000
})
const articles = response.data.articles || []
return articles.map((a: { title?: string }) => a.title).filter((t: string | undefined) => t && t.length > 10)
} catch {
return this.fetchNewsFallback()
}
}
/**
* Fallback news scraper (BBC/CNN headlines)
*/
private async fetchNewsFallback(): Promise<string[]> {
try {
const response = await axios.get('https://www.bbc.com/news', {
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
})
const html = response.data
const regex = /<h3[^>]*>(.*?)<\/h3>/gi
const matches: RegExpMatchArray[] = []
let match
while ((match = regex.exec(html)) !== null) {
matches.push(match)
}
return matches
.map(m => m[1]?.replace(/<[^>]+>/g, '').trim())
.filter((t: string | undefined) => t && t.length > 10 && t.length < 100)
.slice(0, 10) as string[]
} catch {
return []
}
}
/**
* Fetch from Wikipedia (featured articles / trending topics)
*/
private async fetchWikipedia(): Promise<string[]> {
try {
const response = await axios.get('https://en.wikipedia.org/w/api.php', {
params: {
action: 'query',
list: 'random',
rnnamespace: 0,
rnlimit: 15,
format: 'json'
},
timeout: 10000
})
const pages = response.data.query?.random || []
return pages.map((p: { title?: string }) => p.title).filter((t: string | undefined) => t && t.length > 3)
} catch {
return []
}
}
/**
* Local fallback queries (curated list)
*/
private getLocalFallback(count: number): string[] {
const fallback = [
'weather forecast',
'news today',
'stock market',
'sports scores',
'movie reviews',
'recipes',
'travel destinations',
'health tips',
'technology news',
'best restaurants near me',
'how to cook pasta',
'python tutorial',
'world events',
'climate change',
'electric vehicles',
'space exploration',
'artificial intelligence',
'cryptocurrency',
'gaming news',
'fashion trends',
'fitness workout',
'home improvement',
'gardening tips',
'pet care',
'book recommendations',
'music charts',
'streaming shows',
'historical events',
'science discoveries',
'education resources'
]
return this.shuffleArray(fallback).slice(0, count)
}
/**
* Interleave queries from different sources for diversity
*/
private interleaveQueries(queries: string[], targetCount: number): string[] {
const result: string[] = []
const sourceMap = new Map<string, string[]>()
// Group queries by estimated source (simple heuristic)
for (const q of queries) {
const source = this.guessSource(q)
if (!sourceMap.has(source)) {
sourceMap.set(source, [])
}
sourceMap.get(source)?.push(q)
}
const sources = Array.from(sourceMap.values())
let index = 0
while (result.length < targetCount && sources.some(s => s.length > 0)) {
const source = sources[index % sources.length]
if (source && source.length > 0) {
const q = source.shift()
if (q) result.push(q)
}
index++
}
return result
}
/**
* Guess which source a query came from (basic heuristic)
*/
private guessSource(query: string): string {
if (/^[A-Z]/.test(query) && query.includes(' ')) return 'news'
if (query.length > 80) return 'reddit'
if (/how to|what is|why/i.test(query)) return 'local'
return 'trends'
}
/**
* Shuffle array (Fisher-Yates)
*/
private shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!]
}
return shuffled
}
/**
* Clear cache (call between runs)
*/
clearCache(): void {
this.cache.clear()
}
}

177
src/util/RiskManager.ts Normal file
View File

@@ -0,0 +1,177 @@
import { AdaptiveThrottler } from './AdaptiveThrottler'
export interface RiskEvent {
type: 'captcha' | 'error' | 'timeout' | 'ban_hint' | 'success'
timestamp: number
severity: number // 0-10, higher = worse
context?: string
}
export interface RiskMetrics {
score: number // 0-100, higher = riskier
level: 'safe' | 'elevated' | 'high' | 'critical'
recommendation: string
delayMultiplier: number
}
/**
* RiskManager monitors account activity patterns and detects early ban signals.
* Integrates with AdaptiveThrottler to dynamically adjust delays based on risk.
*/
export class RiskManager {
private events: RiskEvent[] = []
private readonly maxEvents = 100
private readonly timeWindowMs = 3600000 // 1 hour
private throttler: AdaptiveThrottler
private cooldownUntil: number = 0
constructor(throttler?: AdaptiveThrottler) {
this.throttler = throttler || new AdaptiveThrottler()
}
/**
* Record a risk event (captcha, error, success, etc.)
*/
recordEvent(type: RiskEvent['type'], severity: number, context?: string): void {
const event: RiskEvent = {
type,
timestamp: Date.now(),
severity: Math.max(0, Math.min(10, severity)),
context
}
this.events.push(event)
if (this.events.length > this.maxEvents) {
this.events.shift()
}
// Feed success/error into adaptive throttler
if (type === 'success') {
this.throttler.record(true)
} else if (['error', 'captcha', 'timeout', 'ban_hint'].includes(type)) {
this.throttler.record(false)
}
// Auto cool-down on critical events
if (severity >= 8) {
const coolMs = Math.min(300000, severity * 30000) // max 5min
this.cooldownUntil = Date.now() + coolMs
}
}
/**
* Calculate current risk metrics based on recent events
*/
assessRisk(): RiskMetrics {
const now = Date.now()
const recentEvents = this.events.filter(e => now - e.timestamp < this.timeWindowMs)
if (recentEvents.length === 0) {
return {
score: 0,
level: 'safe',
recommendation: 'Normal operation',
delayMultiplier: 1.0
}
}
// Calculate base risk score (weighted by recency and severity)
let weightedSum = 0
let totalWeight = 0
for (const event of recentEvents) {
const age = now - event.timestamp
const recencyFactor = 1 - (age / this.timeWindowMs) // newer = higher weight
const weight = recencyFactor * (event.severity / 10)
weightedSum += weight * event.severity
totalWeight += weight
}
const baseScore = totalWeight > 0 ? (weightedSum / totalWeight) * 10 : 0
// Penalty for rapid event frequency
const eventRate = recentEvents.length / (this.timeWindowMs / 60000) // events per minute
const frequencyPenalty = Math.min(30, eventRate * 5)
// Bonus penalty for specific patterns
const captchaCount = recentEvents.filter(e => e.type === 'captcha').length
const banHintCount = recentEvents.filter(e => e.type === 'ban_hint').length
const patternPenalty = (captchaCount * 15) + (banHintCount * 25)
const finalScore = Math.min(100, baseScore + frequencyPenalty + patternPenalty)
// Determine risk level
let level: RiskMetrics['level']
let recommendation: string
let delayMultiplier: number
if (finalScore < 20) {
level = 'safe'
recommendation = 'Normal operation'
delayMultiplier = 1.0
} else if (finalScore < 40) {
level = 'elevated'
recommendation = 'Minor issues detected. Increasing delays slightly.'
delayMultiplier = 1.5
} else if (finalScore < 70) {
level = 'high'
recommendation = 'Significant risk detected. Applying heavy throttling.'
delayMultiplier = 2.5
} else {
level = 'critical'
recommendation = 'CRITICAL: High ban risk. Consider stopping or manual review.'
delayMultiplier = 4.0
}
// Apply adaptive throttler multiplier on top
const adaptiveMultiplier = this.throttler.getDelayMultiplier()
delayMultiplier *= adaptiveMultiplier
return {
score: Math.round(finalScore),
level,
recommendation,
delayMultiplier: Number(delayMultiplier.toFixed(2))
}
}
/**
* Check if currently in forced cool-down period
*/
isInCooldown(): boolean {
return Date.now() < this.cooldownUntil
}
/**
* Get remaining cool-down time in milliseconds
*/
getCooldownRemaining(): number {
const remaining = this.cooldownUntil - Date.now()
return Math.max(0, remaining)
}
/**
* Get the adaptive throttler instance for advanced usage
*/
getThrottler(): AdaptiveThrottler {
return this.throttler
}
/**
* Clear all events and reset state (use between accounts)
*/
reset(): void {
this.events = []
this.cooldownUntil = 0
// Keep throttler state across resets for learning
}
/**
* Export events for analytics/logging
*/
getRecentEvents(limitMinutes: number = 60): RiskEvent[] {
const cutoff = Date.now() - (limitMinutes * 60000)
return this.events.filter(e => e.timestamp >= cutoff)
}
}