mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-10 18:36:17 +00:00
V2.1.5 (#379)
* 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:
15
LICENSE
Normal file
15
LICENSE
Normal 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
128
NOTICE
Normal 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
587
README.md
@@ -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)
|
||||
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://nodejs.org/)
|
||||
[](https://www.docker.com/)
|
||||
[](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 -->
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<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¢er=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">
|
||||
|
||||
[](https://discord.gg/KRBFxxsU)
|
||||
[](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>
|
||||
|
||||
[](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>
|
||||
|
||||
[](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>
|
||||
|
||||

|
||||
|
||||
<br><br>
|
||||
|
||||
**Made with ❤️ by the open source community**
|
||||
|
||||
<br>
|
||||
|
||||

|
||||
|
||||
<br><br>
|
||||
|
||||
<img src="https://capsule-render.vercel.app/api?type=waving&height=120&color=gradient&customColorList=0,2,2,5,6,8§ion=footer" />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
8
package-lock.json
generated
@@ -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": {
|
||||
|
||||
21
package.json
21
package.json
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
67
src/index.ts
67
src/index.ts
@@ -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 {
|
||||
|
||||
@@ -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
264
src/util/Analytics.ts
Normal 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
394
src/util/BanPredictor.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
531
src/util/ConfigValidator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
339
src/util/QueryDiversityEngine.ts
Normal file
339
src/util/QueryDiversityEngine.ts
Normal 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
177
src/util/RiskManager.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user