mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-11 02:46:17 +00:00
2.4.1
THIS IS A HOTFIX TO THE CURRENT STATE TO FIX SOME DOCKER AND STABILITY RELATED ISSUES. ALSO TO REMOVE SOME DEAD CODE. A PROPER VERSION OF "V2" IS BEING WORKED ON! - Migrated configuration files from JSONC to JSON - Removed deprecated setup scripts - Updated dependencies in package.json and package-lock.json - Updated README with expanded setup, configuration, and feature documentation
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,11 +6,7 @@ node_modules/
|
||||
diagnostic/
|
||||
report/
|
||||
accounts.json
|
||||
accounts.jsonc
|
||||
notes
|
||||
accounts.dev.json
|
||||
accounts.dev.jsonc
|
||||
accounts.main.json
|
||||
accounts.main.jsonc
|
||||
.DS_Store
|
||||
.playwright-chromium-installed
|
||||
128
NOTICE
128
NOTICE
@@ -1,128 +0,0 @@
|
||||
# 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.**
|
||||
462
README.md
462
README.md
@@ -2,208 +2,378 @@
|
||||
|
||||
# Quick Setup (Recommended)
|
||||
|
||||
**Easiest way to get started — download and run:**
|
||||
|
||||
1. **Clone the branch** or download the zip.
|
||||
2. **Run the setup script:**
|
||||
|
||||
* **Windows:** double-click `setup/setup.bat` or run it from a command prompt
|
||||
* **Linux / macOS / WSL:**
|
||||
1. Clone this repository or download the latest release ZIP.
|
||||
2. Run the setup script:
|
||||
|
||||
* **Windows:**
|
||||
Double-click `setup/setup.bat`
|
||||
* **Linux / macOS / WSL:**
|
||||
```bash
|
||||
bash setup/setup.sh
|
||||
```
|
||||
* **Alternative (any platform):**
|
||||
|
||||
* **Alternative (any platform):**
|
||||
```bash
|
||||
npm run setup
|
||||
```
|
||||
3. **Follow the setup prompts.** The script will:
|
||||
|
||||
* Rename `accounts.example.json` → `accounts.json`
|
||||
* Ask for Microsoft account credentials
|
||||
3. Follow the prompts — the setup script will:
|
||||
* Copy `accounts.example.json` → `accounts.json`
|
||||
* Ask for your Microsoft account credentials
|
||||
* Remind you to review `config.json`
|
||||
* Install dependencies (`npm install`)
|
||||
* Build the project (`npm run build`)
|
||||
* Build (`npm run build`)
|
||||
* Optionally start the script
|
||||
|
||||
**That's it — the setup script handles the rest.**
|
||||
That's it — the setup script handles the rest.
|
||||
|
||||
---
|
||||
|
||||
# Advanced Setup Options
|
||||
|
||||
### Nix Users
|
||||
|
||||
1. Install Nix from [https://nixos.org/](https://nixos.org/)
|
||||
2. Run:
|
||||
|
||||
If using Nix:
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### Manual Setup (if setup script fails)
|
||||
|
||||
1. Copy `src/accounts.example.json` → `src/accounts.json` and add accounts.
|
||||
2. Edit `src/config.json` as needed.
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. Start:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
1. Copy:
|
||||
```bash
|
||||
cp src/accounts.example.json src/accounts.json
|
||||
```
|
||||
2. Edit `src/accounts.json` and `src/config.json`.
|
||||
3. Install and build:
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Docker Setup (Experimental)
|
||||
# Docker Setup (Recommended for Scheduling)
|
||||
|
||||
**Before starting**
|
||||
## Before Starting
|
||||
* Remove local `/node_modules` and `/dist` if previously built.
|
||||
* Remove old Docker volumes if upgrading from older versions.
|
||||
* You can reuse your old `accounts.json`.
|
||||
|
||||
* Remove local `/node_modules` and `/dist` if you previously built.
|
||||
* Remove old Docker volumes when upgrading from v1.4 or earlier.
|
||||
* You can reuse older `accounts.json`.
|
||||
|
||||
**Quick Docker (recommended for scheduling)**
|
||||
|
||||
1. Clone v2 and configure `accounts.json`.
|
||||
2. Ensure `config.json` has `"headless": true`.
|
||||
## Quick Start
|
||||
1. Clone v2 and configure `accounts.json`
|
||||
2. Ensure `config.json` has `"headless": true`
|
||||
3. Edit `compose.yaml`:
|
||||
|
||||
* Set `TZ` (timezone)
|
||||
* Set `CRON_SCHEDULE` (use crontab.guru for help)
|
||||
* Set your timezone (`TZ`)
|
||||
* Set cron schedule (`CRON_SCHEDULE`)
|
||||
* Optional: `RUN_ON_START=true`
|
||||
4. Start:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
5. Monitor logs:
|
||||
```bash
|
||||
docker logs microsoft-rewards-script
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
The container randomly delays scheduled runs by approximately 5–50 minutes to appear more natural (configurable, see notes below).
|
||||
|
||||
## Example compose.yaml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
microsoft-rewards-script:
|
||||
image: ghcr.io/your-org/microsoft-rewards-script:latest
|
||||
container_name: microsoft-rewards-script
|
||||
restart: unless-stopped
|
||||
|
||||
# Mount your configuration and persistent session storage
|
||||
volumes:
|
||||
# Read-only config files from your working directory into the container
|
||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
||||
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
||||
|
||||
# Persist browser sessions/fingerprints between runs
|
||||
- ./sessions:/usr/src/microsoft-rewards-script/dist/sessions
|
||||
|
||||
# Optional: persist job state directory (if you set jobState.dir to a folder inside dist/)
|
||||
# - ./jobstate:/usr/src/microsoft-rewards-script/dist/jobstate
|
||||
|
||||
environment:
|
||||
# Timezone for scheduling
|
||||
TZ: "Europe/Amsterdam"
|
||||
|
||||
# Node runtime
|
||||
NODE_ENV: "production"
|
||||
|
||||
# Cron schedule for automatic runs (UTC inside container)
|
||||
# Example: run at 07:00, 16:00, and 20:00 every day
|
||||
CRON_SCHEDULE: "0 7,16,20 * * *"
|
||||
|
||||
# Run immediately on container start (in addition to CRON_SCHEDULE)
|
||||
RUN_ON_START: "true"
|
||||
|
||||
# Randomize scheduled start-time between MIN..MAX minutes
|
||||
# Comment these to use defaults (about 5–50 minutes)
|
||||
# MIN_SLEEP_MINUTES: "5"
|
||||
# MAX_SLEEP_MINUTES: "50"
|
||||
|
||||
# Optional: disable randomization entirely
|
||||
# SKIP_RANDOM: "true"
|
||||
|
||||
# Optional: limit resources if desired
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: "1g"
|
||||
```
|
||||
|
||||
5. Monitor:
|
||||
|
||||
```bash
|
||||
docker logs microsoft-rewards-script
|
||||
```
|
||||
|
||||
> The container randomly delays scheduled runs by ~5–50 minutes to appear more natural.
|
||||
|
||||
---
|
||||
|
||||
# Usage Notes
|
||||
|
||||
* **Headless=false cleanup:** If you stop the script without closing browser windows, use Task Manager / `npm run kill-chrome-win` to close leftover instances.
|
||||
* **Scheduling advice:** Run at least twice daily. Use `"runOnZeroPoints": false` in config to skip runs with no points.
|
||||
* **Multiple accounts:** Use `clusters` in `config.json` to run accounts in parallel.
|
||||
### compose.yaml Notes
|
||||
- `volumes`
|
||||
- `accounts.json` and `config.json` are mounted read-only to avoid accidental in-container edits. Edit them on the host.
|
||||
- `sessions` persists your login sessions and fingerprints across restarts and updates.
|
||||
- If you enable `jobState.enabled` and set `jobState.dir`, consider mounting that path as a volume too.
|
||||
- `CRON_SCHEDULE`
|
||||
- Standard crontab format. Use a site like crontab.guru to generate expressions.
|
||||
- The schedule is evaluated inside the container; ensure `TZ` matches your desired timezone.
|
||||
- `RUN_ON_START`
|
||||
- If `"true"`, the script runs once immediately when the container is started, then on the cron schedule.
|
||||
- Randomization
|
||||
- By default, a randomized delay prevents runs from happening at exactly the same time every day.
|
||||
- You can tune it with `MIN_SLEEP_MINUTES` and `MAX_SLEEP_MINUTES`, or disable with `SKIP_RANDOM`.
|
||||
|
||||
---
|
||||
|
||||
# Configuration Reference
|
||||
|
||||
Edit `src/config.json` to customize behavior.
|
||||
Edit `src/config.json` to customize the bot’s behavior.
|
||||
|
||||
### Core Settings (examples)
|
||||
## Core
|
||||
|
||||
| Setting | Description | Default |
|
||||
| ----------------- | -------------------------------: | -------------------------: |
|
||||
| `baseURL` | Microsoft Rewards URL | `https://rewards.bing.com` |
|
||||
| `sessionPath` | Session/fingerprint storage | `sessions` |
|
||||
| `headless` | Run browser in background | `false` |
|
||||
| `parallel` | Run mobile/desktop tasks at once | `true` |
|
||||
| `runOnZeroPoints` | Run when no points available | `false` |
|
||||
| `clusters` | Concurrent account instances | `1` |
|
||||
|
||||
### Fingerprint Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
| ------------------------- | ------------------------: | ------: |
|
||||
| `saveFingerprint.mobile` | Reuse mobile fingerprint | `false` |
|
||||
| `saveFingerprint.desktop` | Reuse desktop fingerprint | `false` |
|
||||
|
||||
### Task Settings (important ones)
|
||||
|
||||
| Setting | Description | Default |
|
||||
| -------------------------- | -----------------: | ------: |
|
||||
| `workers.doDailySet` | Do daily set | `true` |
|
||||
| `workers.doMorePromotions` | Promotional offers | `true` |
|
||||
| `workers.doPunchCards` | Punchcard tasks | `true` |
|
||||
| `workers.doDesktopSearch` | Desktop searches | `true` |
|
||||
| `workers.doMobileSearch` | Mobile searches | `true` |
|
||||
| `workers.doDailyCheckIn` | Daily check-in | `true` |
|
||||
| `workers.doReadToEarn` | Read-to-earn | `true` |
|
||||
|
||||
### Search Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
| ---------------------------------------- | ---------------------: | ------------: |
|
||||
| `searchOnBingLocalQueries` | Use local queries | `false` |
|
||||
| `searchSettings.useGeoLocaleQueries` | Geo-based queries | `false` |
|
||||
| `searchSettings.scrollRandomResults` | Random scrolling | `true` |
|
||||
| `searchSettings.clickRandomResults` | Random link clicks | `true` |
|
||||
| `searchSettings.searchDelay` | Delay between searches | `3-5 minutes` |
|
||||
| `searchSettings.retryMobileSearchAmount` | Mobile retry attempts | `2` |
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
| ------------------------- | --------------------------: | ------------------: |
|
||||
| `globalTimeout` | Action timeout | `30s` |
|
||||
| `logExcludeFunc` | Exclude functions from logs | `SEARCH-CLOSE-TABS` |
|
||||
| `proxy.proxyGoogleTrends` | Proxy Google Trends | `true` |
|
||||
| `proxy.proxyBingTerms` | Proxy Bing Terms | `true` |
|
||||
|
||||
### Webhook Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
| --------------------------- | ---------------------------: | ------: |
|
||||
| `webhook.enabled` | Enable Discord notifications | `false` |
|
||||
| `webhook.url` | Discord webhook URL | `null` |
|
||||
| `conclusionWebhook.enabled` | Summary-only webhook | `false` |
|
||||
| `conclusionWebhook.url` | Summary webhook URL | `null` |
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `baseURL` | Microsoft Rewards base URL | `https://rewards.bing.com` |
|
||||
| `sessionPath` | Folder to store browser sessions | `sessions` |
|
||||
| `dryRun` | Simulate without running tasks | `false` |
|
||||
|
||||
---
|
||||
|
||||
# Features
|
||||
## Browser
|
||||
|
||||
**Account & Session**
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `browser.headless` | Run browser invisibly | `false` |
|
||||
| `browser.globalTimeout` | Timeout for actions | `"30s"` |
|
||||
|
||||
* Multi-account support
|
||||
* Persistent sessions & fingerprints
|
||||
* 2FA support & passwordless options
|
||||
---
|
||||
|
||||
**Automation**
|
||||
## Fingerprinting
|
||||
|
||||
* Headless operation & clustering
|
||||
* Selectable task sets
|
||||
* Proxy support & scheduling (Docker)
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `fingerprinting.saveFingerprint.mobile` | Reuse mobile fingerprint | `true` |
|
||||
| `fingerprinting.saveFingerprint.desktop` | Reuse desktop fingerprint | `true` |
|
||||
|
||||
**Search & Rewards**
|
||||
---
|
||||
|
||||
* Desktop & mobile searches
|
||||
* Emulated browsing, scrolling, clicks
|
||||
* Daily sets, promotions, punchcards, quizzes
|
||||
## Execution
|
||||
|
||||
**Interactions**
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `execution.parallel` | Run desktop and mobile at once | `false` |
|
||||
| `execution.runOnZeroPoints` | Run even with no points | `false` |
|
||||
| `execution.clusters` | Concurrent account clusters | `1` |
|
||||
|
||||
* Quiz solving (10 & 30–40 point variants)
|
||||
* Polls, ABC quizzes, “This or That” answers
|
||||
---
|
||||
|
||||
**Notifications**
|
||||
## Job State
|
||||
|
||||
* Discord webhooks and summary webhooks
|
||||
* Extensive logs for debugging
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `jobState.enabled` | Save last job state | `true` |
|
||||
| `jobState.dir` | Directory for job data | `""` |
|
||||
|
||||
---
|
||||
|
||||
## Workers (Tasks)
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `doDailySet` | Complete daily set | `true` |
|
||||
| `doMorePromotions` | Complete more promotions | `true` |
|
||||
| `doPunchCards` | Complete punchcards | `true` |
|
||||
| `doDesktopSearch` | Perform desktop searches | `true` |
|
||||
| `doMobileSearch` | Perform mobile searches | `true` |
|
||||
| `doDailyCheckIn` | Complete daily check-in | `true` |
|
||||
| `doReadToEarn` | Complete Read-to-Earn | `true` |
|
||||
| `bundleDailySetWithSearch` | Combine daily set and searches | `true` |
|
||||
|
||||
---
|
||||
|
||||
## Search
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `search.useLocalQueries` | Use local query list | `true` |
|
||||
| `search.settings.useGeoLocaleQueries` | Use region-based queries | `true` |
|
||||
| `search.settings.scrollRandomResults` | Random scrolling | `true` |
|
||||
| `search.settings.clickRandomResults` | Random link clicking | `true` |
|
||||
| `search.settings.retryMobileSearchAmount` | Retry mobile searches | `2` |
|
||||
| `search.settings.delay.min` | Minimum delay between searches | `1min` |
|
||||
| `search.settings.delay.max` | Maximum delay between searches | `5min` |
|
||||
|
||||
---
|
||||
|
||||
## Query Diversity
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `queryDiversity.enabled` | Enable multiple query sources | `true` |
|
||||
| `queryDiversity.sources` | Query providers | `["google-trends", "reddit", "local-fallback"]` |
|
||||
| `queryDiversity.maxQueriesPerSource` | Limit per source | `10` |
|
||||
| `queryDiversity.cacheMinutes` | Cache lifetime | `30` |
|
||||
|
||||
---
|
||||
|
||||
## Humanization
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `humanization.enabled` | Enable human behavior | `true` |
|
||||
| `stopOnBan` | Stop immediately on ban | `true` |
|
||||
| `immediateBanAlert` | Alert instantly if banned | `true` |
|
||||
| `actionDelay.min` | Minimum delay per action (ms) | `500` |
|
||||
| `actionDelay.max` | Maximum delay per action (ms) | `2200` |
|
||||
| `gestureMoveProb` | Chance of random mouse movement | `0.65` |
|
||||
| `gestureScrollProb` | Chance of random scrolls | `0.4` |
|
||||
|
||||
---
|
||||
|
||||
## Vacation Mode
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `vacation.enabled` | Enable random pauses | `true` |
|
||||
| `minDays` | Minimum days off | `2` |
|
||||
| `maxDays` | Maximum days off | `4` |
|
||||
|
||||
---
|
||||
|
||||
## Risk Management
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `enabled` | Enable risk-based adjustments | `true` |
|
||||
| `autoAdjustDelays` | Adapt delays dynamically | `true` |
|
||||
| `stopOnCritical` | Stop on critical warning | `false` |
|
||||
| `banPrediction` | Predict bans based on signals | `true` |
|
||||
| `riskThreshold` | Risk tolerance level | `75` |
|
||||
|
||||
---
|
||||
|
||||
## Retry Policy
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `maxAttempts` | Maximum retry attempts | `3` |
|
||||
| `baseDelay` | Initial retry delay | `1000` |
|
||||
| `maxDelay` | Maximum retry delay | `30s` |
|
||||
| `multiplier` | Backoff multiplier | `2` |
|
||||
| `jitter` | Random jitter factor | `0.2` |
|
||||
|
||||
---
|
||||
|
||||
## Proxy
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `proxy.proxyGoogleTrends` | Proxy Google Trends | `true` |
|
||||
| `proxy.proxyBingTerms` | Proxy Bing Terms | `true` |
|
||||
|
||||
---
|
||||
|
||||
## Notifications
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `notifications.webhook.enabled` | Enable Discord webhook | `false` |
|
||||
| `notifications.webhook.url` | Discord webhook URL | `""` |
|
||||
| `notifications.conclusionWebhook.enabled` | Enable summary webhook | `false` |
|
||||
| `notifications.conclusionWebhook.url` | Summary webhook URL | `""` |
|
||||
| `notifications.ntfy.enabled` | Enable Ntfy push alerts | `false` |
|
||||
| `notifications.ntfy.url` | Ntfy server URL | `""` |
|
||||
| `notifications.ntfy.topic` | Ntfy topic name | `"rewards"` |
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
| Setting | Description | Default |
|
||||
|----------|-------------|----------|
|
||||
| `excludeFunc` | Exclude from console logs | `["SEARCH-CLOSE-TABS", "LOGIN-NO-PROMPT", "FLOW"]` |
|
||||
| `webhookExcludeFunc` | Exclude from webhook logs | `["SEARCH-CLOSE-TABS", "LOGIN-NO-PROMPT", "FLOW"]` |
|
||||
| `redactEmails` | Hide emails in logs | `true` |
|
||||
|
||||
---
|
||||
|
||||
# Account Configuration
|
||||
|
||||
Edit `src/accounts.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"enabled": true,
|
||||
"email": "email_1@outlook.com",
|
||||
"password": "password_1",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"email": "email_2@outlook.com",
|
||||
"password": "password_2",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Features Overview
|
||||
|
||||
- Multi-account and session handling
|
||||
- Persistent browser fingerprints
|
||||
- Parallel task execution
|
||||
- Proxy and retry support
|
||||
- Human-like delays and scrolling
|
||||
- Full daily set automation
|
||||
- Mobile and desktop search support
|
||||
- Vacation and risk protection
|
||||
- Webhook and Ntfy notifications
|
||||
- Docker scheduling support
|
||||
|
||||
---
|
||||
|
||||
# Disclaimer
|
||||
|
||||
**Use at your own risk.** Automation may cause suspension or banning of Microsoft Rewards accounts. This project is provided for educational purposes only. The maintainers are **not** responsible for account actions taken by Microsoft.
|
||||
Use at your own risk.
|
||||
Automation of Microsoft Rewards may lead to account suspension or bans.
|
||||
This software is provided for educational purposes only.
|
||||
The authors are not responsible for any actions taken by Microsoft.
|
||||
@@ -6,8 +6,8 @@ services:
|
||||
|
||||
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
||||
volumes:
|
||||
- ./src/accounts.jsonc:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
||||
- ./src/config.jsonc:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
||||
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
||||
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
|
||||
|
||||
environment:
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
playwright-test
|
||||
|
||||
# fixes "waiting until load" issue compared to
|
||||
# setting headless in config.jsonc
|
||||
# setting headless in config.json
|
||||
xvfb-run
|
||||
];
|
||||
|
||||
|
||||
136
package-lock.json
generated
136
package-lock.json
generated
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"axios": "^1.13.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"fingerprint-generator": "^2.1.66",
|
||||
"fingerprint-injector": "^2.1.66",
|
||||
"cron-parser": "^5.4.0",
|
||||
"fingerprint-generator": "^2.1.76",
|
||||
"fingerprint-injector": "^2.1.76",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"luxon": "^3.5.0",
|
||||
@@ -316,6 +316,7 @@
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -584,6 +585,7 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
@@ -661,9 +663,10 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
@@ -677,9 +680,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
|
||||
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
|
||||
"version": "2.8.25",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
||||
"integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
@@ -711,9 +715,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
|
||||
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
|
||||
"version": "4.27.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
||||
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -728,12 +732,13 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
"electron-to-chromium": "^1.5.218",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
"electron-to-chromium": "^1.5.238",
|
||||
"node-releases": "^2.0.26",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -763,9 +768,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001746",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz",
|
||||
"integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==",
|
||||
"version": "1.0.30001754",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -779,7 +784,8 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
@@ -875,15 +881,15 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.4.0.tgz",
|
||||
"integrity": "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
"luxon": "^3.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
@@ -1043,6 +1049,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
|
||||
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-obj": "^2.0.0"
|
||||
},
|
||||
@@ -1073,9 +1080,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.227",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
|
||||
"integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA=="
|
||||
"version": "1.5.249",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz",
|
||||
"integrity": "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
@@ -1151,6 +1159,7 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -1440,12 +1449,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fingerprint-generator": {
|
||||
"version": "2.1.74",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.74.tgz",
|
||||
"integrity": "sha512-GWaUwAiy0hn1EjEG0/aQbjTTISdPN94hWwXwb5riVgvli4+XotLtkIr8aEKBYWb3FcqyW3AbSYgn5OAtPKWLSw==",
|
||||
"version": "2.1.76",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.76.tgz",
|
||||
"integrity": "sha512-nynXZnqCBtBbEgqqdHS5mGm+R9JRRJxNun+lpZlCxGVt0BzgQJGibOvYCe5I54hIIVsaTldZ+jOb4btRPfPD6g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"generative-bayesian-network": "^2.1.74",
|
||||
"header-generator": "^2.1.74",
|
||||
"generative-bayesian-network": "^2.1.76",
|
||||
"header-generator": "^2.1.76",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1453,11 +1463,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fingerprint-injector": {
|
||||
"version": "2.1.74",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.74.tgz",
|
||||
"integrity": "sha512-lTeueOgY0TIT8XGzqFI7IY9pR6wHRxdn3t3PN8g6YNA9ga9A0hc9Ig5xXmi+PFsn/+U4TTtkpxfrWYxYKFGW8g==",
|
||||
"version": "2.1.76",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.76.tgz",
|
||||
"integrity": "sha512-ySlMhYmj7D5ND92BHd/46b6g+izGkj1Vm8qTjSORC3o7HbdFr6eQz8JuL8pVmiJIsC2tsd2ohVvu584HRKurNQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fingerprint-generator": "^2.1.74",
|
||||
"fingerprint-generator": "^2.1.76",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1633,9 +1644,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/generative-bayesian-network": {
|
||||
"version": "2.1.74",
|
||||
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.74.tgz",
|
||||
"integrity": "sha512-AlEQVo1I6FWVObaF8SfPP+qssCv3Heu1Bdu1THMIkB5h4sP9E0AR9zo8qVL936IcdC3tckA4A/fJWhPia7GXgg==",
|
||||
"version": "2.1.76",
|
||||
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.76.tgz",
|
||||
"integrity": "sha512-e9BByo5UEXPsrOii4RM94a02y1JXhP5XZKbzC5GWDz62Bbh2jWbrkY0ta2cF1rxrv8pqLu4c98yQC2F50Eqa7A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
"tslib": "^2.4.0"
|
||||
@@ -1823,12 +1835,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/header-generator": {
|
||||
"version": "2.1.74",
|
||||
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.74.tgz",
|
||||
"integrity": "sha512-0t4mRzBf3mtbwU+Clwm6sDvwEvOM5TIvCicFkYM5z48oR9DGLFPnVNKKOSJIDiJG08r1mY30ovI97JybJ6/Fug==",
|
||||
"version": "2.1.76",
|
||||
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.76.tgz",
|
||||
"integrity": "sha512-Lqk4zU/MIHkm29Sfle6E3Jo2gUoscoG9x12jDt1RbH3kRq/RN+NRSoRRYggmkI0GQSS0wiOIfWwjgIRrA9nHqA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.21.1",
|
||||
"generative-bayesian-network": "^2.1.74",
|
||||
"generative-bayesian-network": "^2.1.76",
|
||||
"ow": "^0.28.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
@@ -1869,6 +1882,7 @@
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
@@ -2002,6 +2016,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2107,7 +2122,8 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
@@ -2213,7 +2229,8 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
@@ -2222,9 +2239,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.21",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
|
||||
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
@@ -2267,6 +2285,7 @@
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz",
|
||||
"integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.2.0",
|
||||
"callsites": "^3.1.0",
|
||||
@@ -2429,7 +2448,8 @@
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -2935,9 +2955,9 @@
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2952,6 +2972,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
@@ -2981,6 +3002,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
|
||||
"integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
||||
27
package.json
27
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
@@ -25,7 +25,6 @@
|
||||
"dev": "ts-node ./src/index.ts -dev",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"prepare": "npm run build",
|
||||
"setup": "node ./setup/update/setup.mjs",
|
||||
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
|
||||
"create-docker": "docker build -t microsoft-rewards-script-docker ."
|
||||
},
|
||||
@@ -39,20 +38,10 @@
|
||||
"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",
|
||||
"@types/node": "^20.14.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-modules-newline": "^0.0.6",
|
||||
@@ -60,19 +49,19 @@
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"cron-parser": "^4.9.0",
|
||||
"axios": "^1.13.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"fingerprint-generator": "^2.1.66",
|
||||
"fingerprint-injector": "^2.1.66",
|
||||
"cron-parser": "^5.4.0",
|
||||
"fingerprint-generator": "^2.1.76",
|
||||
"fingerprint-injector": "^2.1.76",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ms": "^2.1.3",
|
||||
"luxon": "^3.5.0",
|
||||
"ms": "^2.1.3",
|
||||
"playwright": "1.52.0",
|
||||
"rebrowser-playwright": "1.52.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Unified cross-platform setup script for Microsoft Rewards Script V2.
|
||||
*
|
||||
* Features:
|
||||
* - Renames accounts.example.jsonc -> 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';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
// Project root = two levels up from setup/update directory
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
||||
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
||||
|
||||
function log(msg) { console.log(msg); }
|
||||
function warn(msg) { console.warn(msg); }
|
||||
function error(msg) { console.error(msg); }
|
||||
|
||||
function renameAccountsIfNeeded() {
|
||||
const accounts = path.join(SRC_DIR, 'accounts.json');
|
||||
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
|
||||
if (fs.existsSync(accounts)) {
|
||||
log('accounts.json already exists - skipping rename.');
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(example)) {
|
||||
log('Renaming accounts.example.jsonc to accounts.json...');
|
||||
fs.renameSync(example, accounts);
|
||||
} else {
|
||||
warn('Neither accounts.json nor accounts.example.jsonc found.');
|
||||
}
|
||||
}
|
||||
|
||||
async function prompt(question) {
|
||||
return await new Promise(resolve => {
|
||||
process.stdout.write(question);
|
||||
const onData = (data) => {
|
||||
const ans = data.toString().trim();
|
||||
process.stdin.off('data', onData);
|
||||
resolve(ans);
|
||||
};
|
||||
process.stdin.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
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 configured your accounts in accounts.json? (yes/no): ')).toLowerCase();
|
||||
if (['yes', 'y'].includes(ans)) break;
|
||||
if (['no', 'n'].includes(ans)) {
|
||||
log('Please configure accounts.json and save the file, then answer yes.');
|
||||
continue;
|
||||
}
|
||||
log('Please answer yes or no.');
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(cmd, args, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
log(`Running: ${cmd} ${args.join(' ')}`);
|
||||
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) return resolve();
|
||||
reject(new Error(`${cmd} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureNpmAvailable() {
|
||||
try {
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v']);
|
||||
} catch (e) {
|
||||
throw new Error('npm not found in PATH. Install Node.js first.');
|
||||
}
|
||||
}
|
||||
|
||||
async function startOnly() {
|
||||
log('Starting program (npm run start)...');
|
||||
await ensureNpmAvailable();
|
||||
// Assume user already installed & built; if dist missing inform user.
|
||||
const distIndex = path.join(PROJECT_ROOT, 'dist', 'index.js');
|
||||
if (!fs.existsSync(distIndex)) {
|
||||
warn('Build output not found. Running build first.');
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
|
||||
await installPlaywrightBrowsers();
|
||||
} else {
|
||||
// Even if build exists, ensure browsers are installed once.
|
||||
await installPlaywrightBrowsers();
|
||||
}
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']);
|
||||
}
|
||||
|
||||
async function fullSetup() {
|
||||
renameAccountsIfNeeded();
|
||||
await loopForAccountsConfirmation();
|
||||
|
||||
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();
|
||||
|
||||
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('\nFinished setup. To start later, run: npm start');
|
||||
log('For automated scheduling, run: npm run start:schedule');
|
||||
}
|
||||
}
|
||||
|
||||
async function installPlaywrightBrowsers() {
|
||||
const PLAYWRIGHT_MARKER = path.join(PROJECT_ROOT, '.playwright-chromium-installed');
|
||||
// Idempotent: skip if marker exists
|
||||
if (fs.existsSync(PLAYWRIGHT_MARKER)) {
|
||||
log('Playwright chromium already installed (marker found).');
|
||||
return;
|
||||
}
|
||||
log('Ensuring Playwright chromium browser is installed...');
|
||||
try {
|
||||
await runCommand(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['playwright', 'install', 'chromium']);
|
||||
fs.writeFileSync(PLAYWRIGHT_MARKER, new Date().toISOString());
|
||||
log('Playwright chromium install complete.');
|
||||
} catch (e) {
|
||||
warn('Failed to install Playwright chromium automatically. You can manually run: npx playwright install chromium');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(SRC_DIR)) {
|
||||
error('[ERROR] Cannot find src directory at ' + SRC_DIR);
|
||||
process.exit(1);
|
||||
}
|
||||
process.chdir(PROJECT_ROOT);
|
||||
|
||||
for (;;) {
|
||||
log('============================');
|
||||
log(' Microsoft Rewards Setup ');
|
||||
log('============================');
|
||||
log('Select an option:');
|
||||
log(' 1) Start program now (skip setup)');
|
||||
log(' 2) Full first-time setup');
|
||||
log(' 3) Exit');
|
||||
const choice = (await prompt('Enter choice (1/2/3): ')).trim();
|
||||
if (choice === '1') { await startOnly(); break; }
|
||||
if (choice === '2') { await fullSetup(); break; }
|
||||
if (choice === '3') { log('Exiting.'); process.exit(0); }
|
||||
log('\nInvalid choice. Please select 1, 2 or 3.\n');
|
||||
}
|
||||
// After completing action, optionally pause if launched by double click on Windows (no TTY detection simple heuristic)
|
||||
if (process.platform === 'win32' && process.stdin.isTTY) {
|
||||
log('\nDone. Press Enter to close.');
|
||||
await prompt('');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Allow clean Ctrl+C
|
||||
process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1); });
|
||||
|
||||
main().catch(err => {
|
||||
error('\nSetup failed: ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,225 +0,0 @@
|
||||
/* eslint-disable linebreak-style */
|
||||
/**
|
||||
* Smart Auto-Update Script
|
||||
*
|
||||
* Intelligently updates while preserving user settings:
|
||||
* - ALWAYS updates code files (*.ts, *.js, etc.)
|
||||
* - ONLY updates config.jsonc if remote has changes to it
|
||||
* - ONLY updates accounts.json if remote has changes to it
|
||||
* - KEEPS user passwords/emails/settings otherwise
|
||||
*
|
||||
* Usage:
|
||||
* node setup/update/update.mjs --git
|
||||
* node setup/update/update.mjs --docker
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts })
|
||||
child.on('close', (code) => resolve(code ?? 0))
|
||||
child.on('error', () => resolve(1))
|
||||
})
|
||||
}
|
||||
|
||||
async function which(cmd) {
|
||||
const probe = process.platform === 'win32' ? 'where' : 'which'
|
||||
const code = await run(probe, [cmd], { stdio: 'ignore' })
|
||||
return code === 0
|
||||
}
|
||||
|
||||
function exec(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGit() {
|
||||
const hasGit = await which('git')
|
||||
if (!hasGit) {
|
||||
console.log('Git not found. Skipping update.')
|
||||
return 1
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('Smart Git Update')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
// Step 1: Read config to get user preferences
|
||||
let userConfig = { autoUpdateConfig: false, autoUpdateAccounts: false }
|
||||
try {
|
||||
if (existsSync('src/config.jsonc')) {
|
||||
const configContent = readFileSync('src/config.jsonc', 'utf8')
|
||||
.replace(/\/\/.*$/gm, '') // remove comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
||||
const config = JSON.parse(configContent)
|
||||
if (config.update) {
|
||||
userConfig.autoUpdateConfig = config.update.autoUpdateConfig ?? false
|
||||
userConfig.autoUpdateAccounts = config.update.autoUpdateAccounts ?? false
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Warning: Could not read config.jsonc, using defaults (preserve local files)')
|
||||
}
|
||||
|
||||
console.log('\nUser preferences:')
|
||||
console.log(` Auto-update config.jsonc: ${userConfig.autoUpdateConfig}`)
|
||||
console.log(` Auto-update accounts.json: ${userConfig.autoUpdateAccounts}`)
|
||||
|
||||
// Step 2: Fetch
|
||||
console.log('\nFetching latest changes...')
|
||||
await run('git', ['fetch', '--all', '--prune'])
|
||||
|
||||
// Step 3: Get current branch
|
||||
const currentBranch = exec('git branch --show-current')
|
||||
if (!currentBranch) {
|
||||
console.log('Could not determine current branch.')
|
||||
return 1
|
||||
}
|
||||
|
||||
// Step 4: Check which files changed in remote
|
||||
const remoteBranch = `origin/${currentBranch}`
|
||||
const filesChanged = exec(`git diff --name-only HEAD ${remoteBranch}`)
|
||||
|
||||
if (!filesChanged) {
|
||||
console.log('Already up to date!')
|
||||
return 0
|
||||
}
|
||||
|
||||
const changedFiles = filesChanged.split('\n').filter(f => f.trim())
|
||||
const configChanged = changedFiles.includes('src/config.jsonc')
|
||||
const accountsChanged = changedFiles.includes('src/accounts.json')
|
||||
|
||||
// Step 5: ALWAYS backup config and accounts (smart strategy!)
|
||||
const backupDir = join(process.cwd(), '.update-backup')
|
||||
mkdirSync(backupDir, { recursive: true })
|
||||
|
||||
const filesToRestore = []
|
||||
|
||||
if (existsSync('src/config.jsonc')) {
|
||||
console.log('\nBacking up config.jsonc...')
|
||||
writeFileSync(join(backupDir, 'config.jsonc'), readFileSync('src/config.jsonc', 'utf8'))
|
||||
// Restore if: remote changed it AND user doesn't want auto-update
|
||||
if (configChanged && !userConfig.autoUpdateConfig) {
|
||||
filesToRestore.push('config.jsonc')
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync('src/accounts.json')) {
|
||||
console.log('Backing up accounts.json...')
|
||||
writeFileSync(join(backupDir, 'accounts.json'), readFileSync('src/accounts.json', 'utf8'))
|
||||
// Restore if: remote changed it AND user doesn't want auto-update
|
||||
if (accountsChanged && !userConfig.autoUpdateAccounts) {
|
||||
filesToRestore.push('accounts.json')
|
||||
}
|
||||
}
|
||||
|
||||
// Show what will happen
|
||||
console.log('\nRemote changes:')
|
||||
if (configChanged) {
|
||||
console.log(` config.jsonc: ${userConfig.autoUpdateConfig ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
|
||||
} else {
|
||||
console.log(' config.jsonc: no changes in remote')
|
||||
}
|
||||
if (accountsChanged) {
|
||||
console.log(` accounts.json: ${userConfig.autoUpdateAccounts ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
|
||||
} else {
|
||||
console.log(' accounts.json: no changes in remote')
|
||||
}
|
||||
|
||||
// Step 6: Stash changes
|
||||
const hasChanges = exec('git status --porcelain')
|
||||
if (hasChanges) {
|
||||
console.log('\nStashing local changes...')
|
||||
await run('git', ['stash', 'push', '-m', 'Auto-update backup'])
|
||||
}
|
||||
|
||||
// Step 7: Pull
|
||||
console.log('\nPulling latest code...')
|
||||
const pullCode = await run('git', ['pull', '--rebase'])
|
||||
|
||||
if (pullCode !== 0) {
|
||||
console.log('Pull failed.')
|
||||
if (hasChanges) await run('git', ['stash', 'pop'])
|
||||
return pullCode
|
||||
}
|
||||
|
||||
// Step 8: Restore files based on user preferences
|
||||
if (filesToRestore.length > 0) {
|
||||
console.log('\nRestoring local files (per your config preferences)...')
|
||||
for (const file of filesToRestore) {
|
||||
const content = readFileSync(join(backupDir, file), 'utf8')
|
||||
writeFileSync(join('src', file), content)
|
||||
console.log(` ✓ Restored ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 9: Restore stash
|
||||
if (hasChanges) {
|
||||
await run('git', ['stash', 'pop'])
|
||||
}
|
||||
|
||||
// Step 9: Install & build
|
||||
const hasNpm = await which('npm')
|
||||
if (!hasNpm) return 0
|
||||
|
||||
console.log('\nInstalling dependencies...')
|
||||
await run('npm', ['ci'])
|
||||
|
||||
console.log('\nBuilding project...')
|
||||
const buildCode = await run('npm', ['run', 'build'])
|
||||
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('Update completed!')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
// Optional: Show community info for advanced features
|
||||
if (Math.random() > 0.7) {
|
||||
console.log('\n💡 Tip: Looking for faster updates and advanced features?')
|
||||
console.log(' Check out enhanced builds with better anti-detection.')
|
||||
console.log(' Community: https://discord.gg/kn3695Kx32')
|
||||
}
|
||||
console.log('')
|
||||
|
||||
return buildCode
|
||||
}
|
||||
|
||||
async function updateDocker() {
|
||||
const hasDocker = await which('docker')
|
||||
if (!hasDocker) return 1
|
||||
// Prefer compose v2 (docker compose)
|
||||
await run('docker', ['compose', 'pull'])
|
||||
return run('docker', ['compose', 'up', '-d'])
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = new Set(process.argv.slice(2))
|
||||
const doGit = args.has('--git')
|
||||
const doDocker = args.has('--docker')
|
||||
|
||||
let code = 0
|
||||
if (doGit) {
|
||||
code = await updateGit()
|
||||
}
|
||||
if (doDocker && code === 0) {
|
||||
code = await updateDocker()
|
||||
}
|
||||
|
||||
// Only exit if not called from scheduler
|
||||
// When FROM_SCHEDULER=1, the parent script will handle process lifecycle
|
||||
if (process.env.FROM_SCHEDULER !== '1') {
|
||||
process.exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(() => {
|
||||
// Only exit on error if not called from scheduler
|
||||
if (process.env.FROM_SCHEDULER !== '1') {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
32
src/accounts.example.json
Normal file
32
src/accounts.example.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"enabled": true,
|
||||
"email": "email_1@outlook.com",
|
||||
"password": "password_1",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"email": "email_2@outlook.com",
|
||||
"password": "password_2",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
{
|
||||
// ============================================================
|
||||
// 📧 MICROSOFT ACCOUNTS CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
// ⚠️ IMPORTANT SECURITY NOTICE
|
||||
// This file contains sensitive credentials. Never commit the real accounts.jsonc to version control.
|
||||
// The .gitignore is configured to exclude accounts.jsonc but you should verify it's not tracked.
|
||||
|
||||
// 📊 MICROSOFT ACCOUNT LIMITS (Unofficial Guidelines)
|
||||
// - New accounts per IP per day: ~3 (official soft limit)
|
||||
// - Recommended active accounts per household IP: ~5 (to avoid suspicion)
|
||||
// - Creating too many accounts quickly may trigger verification (phone, OTP, captcha)
|
||||
// - Unusual activity can result in temporary blocks or account restrictions
|
||||
|
||||
"accounts": [
|
||||
{
|
||||
// ============================================================
|
||||
// 👤 ACCOUNT 1
|
||||
// ============================================================
|
||||
|
||||
// Enable or disable this account (true = active, false = skip)
|
||||
"enabled": true,
|
||||
|
||||
// Microsoft account email address
|
||||
"email": "email_1@outlook.com",
|
||||
|
||||
// Account password
|
||||
"password": "password_1",
|
||||
|
||||
// Two-Factor Authentication (2FA) TOTP secret (optional but HIGHLY recommended for security)
|
||||
// Get this from your authenticator app (e.g., Microsoft Authenticator, Google Authenticator)
|
||||
// Format: base32 secret key (e.g., "JBSWY3DPEHPK3PXP")
|
||||
// Leave empty "" if 2FA is not enabled
|
||||
"totp": "",
|
||||
|
||||
// ⚠️ REQUIRED: Recovery email address associated with this Microsoft account
|
||||
// During login, Microsoft shows the first 2 characters and the domain of the recovery email (e.g., "ab***@example.com")
|
||||
// This field is MANDATORY to detect account compromise or bans:
|
||||
// - The script compares what Microsoft displays with this configured recovery email
|
||||
// - If they don't match, it alerts you that the account may be compromised or the recovery email was changed
|
||||
// - This security check helps identify hijacked accounts before they cause issues
|
||||
// Format: Full recovery email address (e.g., "backup@gmail.com")
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
|
||||
// ============================================================
|
||||
// 🌐 PROXY CONFIGURATION (Optional)
|
||||
// ============================================================
|
||||
|
||||
"proxy": {
|
||||
// Enable proxy for HTTP requests (axios/API calls)
|
||||
// If false, proxy is only used for browser automation
|
||||
"proxyAxios": true,
|
||||
|
||||
// Proxy server URL (without protocol)
|
||||
// Examples: "proxy.example.com", "123.45.67.89"
|
||||
// Leave empty "" to disable proxy for this account
|
||||
"url": "",
|
||||
|
||||
// Proxy port number
|
||||
"port": 0,
|
||||
|
||||
// Proxy authentication username (leave empty if no auth required)
|
||||
"username": "",
|
||||
|
||||
// Proxy authentication password (leave empty if no auth required)
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// ============================================================
|
||||
// 👤 ACCOUNT 2
|
||||
// ============================================================
|
||||
|
||||
"enabled": false,
|
||||
"email": "email_2@outlook.com",
|
||||
"password": "password_2",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// ============================================================
|
||||
// 👤 ACCOUNT 3
|
||||
// ============================================================
|
||||
|
||||
"enabled": false,
|
||||
"email": "email_3@outlook.com",
|
||||
"password": "password_3",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// ============================================================
|
||||
// 👤 ACCOUNT 4
|
||||
// ============================================================
|
||||
|
||||
"enabled": false,
|
||||
"email": "email_4@outlook.com",
|
||||
"password": "password_4",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// ============================================================
|
||||
// 👤 ACCOUNT 5
|
||||
// ============================================================
|
||||
|
||||
"enabled": false,
|
||||
"email": "email_5@outlook.com",
|
||||
"password": "password_5",
|
||||
"totp": "",
|
||||
"recoveryEmail": "your_email@domain.com",
|
||||
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,23 +25,14 @@ class Browser {
|
||||
}
|
||||
|
||||
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
|
||||
// Optional automatic browser installation (set AUTO_INSTALL_BROWSERS=1)
|
||||
if (process.env.AUTO_INSTALL_BROWSERS === '1') {
|
||||
try {
|
||||
// Dynamically import child_process to avoid overhead otherwise
|
||||
const { execSync } = await import('child_process')
|
||||
execSync('npx playwright install chromium', { stdio: 'ignore' })
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
let browser: import('rebrowser-playwright').Browser
|
||||
let browser: playwright.Browser
|
||||
try {
|
||||
// FORCE_HEADLESS env takes precedence (used in Docker with headless shell only)
|
||||
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
|
||||
// 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)
|
||||
const headlessValue = envForceHeadless ? true : (legacyHeadless ?? nestedHeadless ?? false)
|
||||
const headless: boolean = Boolean(headlessValue)
|
||||
|
||||
const engineName = 'chromium' // current hard-coded engine
|
||||
@@ -64,29 +55,29 @@ class Browser {
|
||||
const msg = (e instanceof Error ? e.message : String(e))
|
||||
// Common missing browser executable guidance
|
||||
if (/Executable doesn't exist/i.test(msg)) {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run "npm run pre-build" to install all dependencies (or set AUTO_INSTALL_BROWSERS=1 to auto-attempt).', 'error')
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run "npm run pre-build" to install all dependencies', 'error')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
// Resolve saveFingerprint from legacy root or new fingerprinting.saveFingerprint
|
||||
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 }
|
||||
// Resolve saveFingerprint from legacy root or new fingerprinting.saveFingerprint
|
||||
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)
|
||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, saveFingerprint)
|
||||
|
||||
const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint()
|
||||
|
||||
const context = await newInjectedContext(browser as unknown as import('playwright').Browser, { fingerprint: fingerprint })
|
||||
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 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))
|
||||
// Set timeout to preferred amount (supports legacy globalTimeout or browser.globalTimeout)
|
||||
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 {
|
||||
|
||||
@@ -25,80 +25,134 @@ export default class BrowserFunc {
|
||||
* @param {Page} page Playwright page
|
||||
*/
|
||||
async goHome(page: Page) {
|
||||
const navigateHome = async () => {
|
||||
try {
|
||||
await page.goto(this.bot.config.baseURL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
} catch (e: any) {
|
||||
if (typeof e?.message === 'string' && e.message.includes('ERR_ABORTED')) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Navigation aborted, retrying...', 'warn')
|
||||
await this.bot.utils.wait(1500)
|
||||
await page.goto(this.bot.config.baseURL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
|
||||
if (page.url() === dashboardURL.href) {
|
||||
return
|
||||
if (new URL(page.url()).hostname !== dashboardURL.hostname) {
|
||||
await navigateHome()
|
||||
}
|
||||
|
||||
await page.goto(this.bot.config.baseURL)
|
||||
let success = false
|
||||
|
||||
for (let iteration = 1; iteration <= RETRY_LIMITS.GO_HOME_MAX; iteration++) {
|
||||
await this.bot.utils.wait(TIMEOUTS.LONG)
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
try {
|
||||
// If activities are found, exit the loop (SUCCESS - account is OK)
|
||||
await page.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: 1000 })
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
|
||||
success = true
|
||||
break
|
||||
} catch {
|
||||
const suspendedByHeader = await page
|
||||
.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
} catch (error) {
|
||||
// Activities not found yet - check if it's because account is suspended
|
||||
// Only check suspension if we can't find activities (reduces false positives)
|
||||
const suspendedByHeader = await page.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 }).then(() => true).catch(() => false)
|
||||
|
||||
if (suspendedByHeader) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error')
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'GO-HOME',
|
||||
`Account suspension detected by header selector (iteration ${iteration})`,
|
||||
'error'
|
||||
)
|
||||
throw new Error('Account has been suspended!')
|
||||
}
|
||||
|
||||
// Secondary check: look for suspension text in main content area only
|
||||
|
||||
try {
|
||||
const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || ''
|
||||
const mainContent =
|
||||
(await page
|
||||
.locator('#contentContainer, #main, .main-content')
|
||||
.first()
|
||||
.textContent({ timeout: 500 })
|
||||
.catch(() => '')) || ''
|
||||
|
||||
const suspensionPatterns = [
|
||||
/account\s+has\s+been\s+suspended/i,
|
||||
/suspended\s+due\s+to\s+unusual\s+activity/i,
|
||||
/your\s+account\s+is\s+temporarily\s+suspended/i
|
||||
]
|
||||
|
||||
const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent))
|
||||
|
||||
const isSuspended = suspensionPatterns.some((p) => p.test(mainContent))
|
||||
if (isSuspended) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'GO-HOME',
|
||||
`Account suspension detected by content text (iteration ${iteration})`,
|
||||
'error'
|
||||
)
|
||||
throw new Error('Account has been suspended!')
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors in text check - not critical
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${e}`, 'warn')
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'GO-HOME',
|
||||
`Suspension text check skipped: ${e instanceof Error ? e.message : String(e)}`,
|
||||
'warn'
|
||||
)
|
||||
}
|
||||
|
||||
const currentURL = new URL(page.url())
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
|
||||
try {
|
||||
await navigateHome()
|
||||
} catch (e: any) {
|
||||
if (typeof e?.message === 'string' && e.message.includes('ERR_ABORTED')) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Navigation aborted again; continuing...', 'warn')
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'GO-HOME',
|
||||
`Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`,
|
||||
'warn'
|
||||
)
|
||||
}
|
||||
|
||||
// Not suspended, just activities not loaded yet - continue to next iteration
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn')
|
||||
}
|
||||
|
||||
// Below runs if the homepage was unable to be visited
|
||||
const currentURL = new URL(page.url())
|
||||
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
|
||||
await page.goto(this.bot.config.baseURL)
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
|
||||
break
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(TIMEOUTS.VERY_LONG)
|
||||
const backoff = Math.min(TIMEOUTS.VERY_LONG, 1000 + iteration * 500)
|
||||
await this.bot.utils.wait(backoff)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Failed to reach homepage or find activities within retry limit')
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred:' + error, 'error')
|
||||
throw this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'GO-HOME',
|
||||
'An error occurred:' + (error instanceof Error ? ` ${error.message}` : ` ${String(error)}`),
|
||||
'error'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch user dashboard data
|
||||
* @returns {DashboardData} Object of user bing rewards dashboard data
|
||||
@@ -114,7 +168,7 @@ export default class BrowserFunc {
|
||||
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||
await this.goHome(target)
|
||||
}
|
||||
let lastError: unknown = null
|
||||
let lastError: unknown = null
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
// Reload the page to get new data
|
||||
@@ -131,7 +185,7 @@ export default class BrowserFunc {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
||||
try {
|
||||
await this.goHome(target)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -143,7 +197,7 @@ export default class BrowserFunc {
|
||||
|
||||
// Wait a bit longer for scripts to load, especially on mobile
|
||||
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
|
||||
|
||||
|
||||
// Wait for the more-activities element to ensure page is fully loaded
|
||||
await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch(() => {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Activities element not found, continuing anyway', 'warn')
|
||||
@@ -158,7 +212,7 @@ export default class BrowserFunc {
|
||||
|
||||
if (!scriptContent) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn')
|
||||
|
||||
|
||||
// Force a navigation retry once before failing hard
|
||||
try {
|
||||
await this.goHome(target)
|
||||
@@ -169,20 +223,20 @@ export default class BrowserFunc {
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Recovery navigation failed: ${e}`, 'warn')
|
||||
}
|
||||
|
||||
|
||||
const retryContent = await target.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
return targetScript?.innerText ? targetScript.innerText : null
|
||||
}).catch(()=>null)
|
||||
|
||||
}).catch(() => null)
|
||||
|
||||
if (!retryContent) {
|
||||
// Log additional debug info
|
||||
const scriptsDebug = await target.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
return scripts.map(s => s.innerText.substring(0, 100)).join(' | ')
|
||||
}).catch(() => 'Unable to get script debug info')
|
||||
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Available scripts preview: ${scriptsDebug}`, 'warn')
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
||||
}
|
||||
@@ -422,10 +476,10 @@ export default class BrowserFunc {
|
||||
.map(el => $(el).text())
|
||||
.filter(t => t.length > 0)
|
||||
.map(t => t.substring(0, 100))
|
||||
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn')
|
||||
|
||||
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||
}
|
||||
|
||||
@@ -471,10 +525,10 @@ export default class BrowserFunc {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
if (element) {
|
||||
selector = `a[href*="${element.attribs.href}"]`
|
||||
}
|
||||
|
||||
121
src/config.json
Normal file
121
src/config.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
"sessionPath": "sessions",
|
||||
"dryRun": false,
|
||||
"browser": {
|
||||
"headless": false,
|
||||
"globalTimeout": "30s"
|
||||
},
|
||||
"fingerprinting": {
|
||||
"saveFingerprint": {
|
||||
"mobile": true,
|
||||
"desktop": true
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"parallel": false,
|
||||
"runOnZeroPoints": false,
|
||||
"clusters": 1
|
||||
},
|
||||
"jobState": {
|
||||
"enabled": true,
|
||||
"dir": ""
|
||||
},
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doDesktopSearch": true,
|
||||
"doMobileSearch": true,
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true,
|
||||
"bundleDailySetWithSearch": true
|
||||
},
|
||||
"search": {
|
||||
"useLocalQueries": true,
|
||||
"settings": {
|
||||
"useGeoLocaleQueries": true,
|
||||
"scrollRandomResults": true,
|
||||
"clickRandomResults": true,
|
||||
"retryMobileSearchAmount": 2,
|
||||
"delay": {
|
||||
"min": "2min",
|
||||
"max": "5min"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queryDiversity": {
|
||||
"enabled": true,
|
||||
"sources": [
|
||||
"google-trends",
|
||||
"reddit",
|
||||
"local-fallback"
|
||||
],
|
||||
"maxQueriesPerSource": 10,
|
||||
"cacheMinutes": 30
|
||||
},
|
||||
"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
|
||||
},
|
||||
"riskManagement": {
|
||||
"enabled": true,
|
||||
"autoAdjustDelays": true,
|
||||
"stopOnCritical": false,
|
||||
"banPrediction": true,
|
||||
"riskThreshold": 75
|
||||
},
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 3,
|
||||
"baseDelay": 1000,
|
||||
"maxDelay": "30s",
|
||||
"multiplier": 2,
|
||||
"jitter": 0.2
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
241
src/config.jsonc
241
src/config.jsonc
@@ -1,241 +0,0 @@
|
||||
{
|
||||
// ============================================================
|
||||
// 🌐 GENERAL CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
// Base URL for Microsoft Rewards dashboard (do not change unless necessary)
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
|
||||
// Directory to store sessions (cookies, browser fingerprints)
|
||||
"sessionPath": "sessions",
|
||||
|
||||
// Dry-run mode: simulate execution without actually running tasks (useful for testing)
|
||||
"dryRun": false,
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🖥️ BROWSER CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
"browser": {
|
||||
// false = visible window | true = headless mode (invisible)
|
||||
"headless": false,
|
||||
// Max timeout for operations (supports: 30000, "30s", "2min")
|
||||
"globalTimeout": "30s"
|
||||
},
|
||||
|
||||
"fingerprinting": {
|
||||
// Persist browser fingerprints to improve consistency across runs
|
||||
"saveFingerprint": {
|
||||
"mobile": true,
|
||||
"desktop": true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ⚙️ EXECUTION & PERFORMANCE
|
||||
// ============================================================
|
||||
|
||||
"execution": {
|
||||
// true = Desktop + Mobile in parallel (faster, more resources)
|
||||
// false = Sequential (slower, fewer resources)
|
||||
"parallel": false,
|
||||
// If false, skip execution when 0 points are available
|
||||
"runOnZeroPoints": false,
|
||||
// Number of account clusters (processes) to run concurrently
|
||||
"clusters": 1,
|
||||
// How many times to run through all accounts in sequence (1 = process each account once, 2 = twice, etc.)
|
||||
// Higher values can catch missed tasks but increase detection risk
|
||||
"passesPerRun": 3
|
||||
},
|
||||
|
||||
|
||||
"jobState": {
|
||||
// Save state to avoid duplicate work across restarts
|
||||
"enabled": true,
|
||||
// Custom state directory (empty = defaults to sessionPath/job-state)
|
||||
"dir": ""
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🎯 TASKS & WORKERS
|
||||
// ============================================================
|
||||
|
||||
"workers": {
|
||||
// Select which tasks the bot should complete on desktop/mobile
|
||||
"doDailySet": true, // Daily set tasks
|
||||
"doMorePromotions": true, // More promotions section
|
||||
"doPunchCards": true, // Punch cards
|
||||
"doDesktopSearch": true, // Desktop searches
|
||||
"doMobileSearch": true, // Mobile searches
|
||||
"doDailyCheckIn": true, // Daily check-in
|
||||
"doReadToEarn": true, // Read to earn
|
||||
// If true, run desktop searches right after Daily Set
|
||||
"bundleDailySetWithSearch": true
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🔍 SEARCH CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
"search": {
|
||||
// Use locale-specific query sources
|
||||
"useLocalQueries": true,
|
||||
"settings": {
|
||||
// Use region-specific queries (at, fr, us, etc.)
|
||||
"useGeoLocaleQueries": true,
|
||||
// Randomly scroll search result pages (more natural behavior)
|
||||
"scrollRandomResults": true,
|
||||
// Occasionally click a result (safe targets only)
|
||||
"clickRandomResults": true,
|
||||
// Number of retries if mobile searches don't progress
|
||||
"retryMobileSearchAmount": 2,
|
||||
// Delay between searches
|
||||
"delay": {
|
||||
"min": "1min",
|
||||
"max": "5min"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"queryDiversity": {
|
||||
// Multi-source query generation: Reddit, News, Wikipedia instead of only Google Trends
|
||||
"enabled": true,
|
||||
// Available sources: 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
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🤖 HUMANIZATION & NATURAL BEHAVIOR
|
||||
// ============================================================
|
||||
|
||||
"humanization": {
|
||||
// Human Mode: adds subtle micro-gestures & pauses to mimic real users
|
||||
"enabled": true,
|
||||
// If a ban is detected on any account, stop processing remaining accounts
|
||||
"stopOnBan": true,
|
||||
// Immediately send an alert (webhook/NTFY) when a ban is detected
|
||||
"immediateBanAlert": true,
|
||||
// Extra random pause between actions
|
||||
"actionDelay": {
|
||||
"min": 500, // 0.5 seconds minimum
|
||||
"max": 2200 // 2.2 seconds maximum
|
||||
},
|
||||
// Probability (0-1) to move mouse slightly between actions
|
||||
"gestureMoveProb": 0.65,
|
||||
// Probability (0-1) to perform a small scroll
|
||||
"gestureScrollProb": 0.4,
|
||||
// Optional execution time windows (e.g., ["08:30-11:00", "19:00-22:00"])
|
||||
// If specified, waits until inside a window before starting
|
||||
"allowedWindows": []
|
||||
},
|
||||
|
||||
"vacation": {
|
||||
// Monthly "vacation" block: skip a random range of days each month
|
||||
// Each month, a random period between minDays and maxDays is selected
|
||||
// and all runs within that date range are skipped (more human-like behavior)
|
||||
"enabled": true,
|
||||
"minDays": 2,
|
||||
"maxDays": 4
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🛡️ RISK MANAGEMENT & SECURITY
|
||||
// ============================================================
|
||||
|
||||
"riskManagement": {
|
||||
// Dynamic delay adjustment based on detected risk signals
|
||||
"enabled": true,
|
||||
// Automatically increase delays when captchas/errors are detected
|
||||
"autoAdjustDelays": true,
|
||||
// Stop execution if risk level reaches critical threshold
|
||||
"stopOnCritical": false,
|
||||
// Enable ML-based ban prediction based on patterns
|
||||
"banPrediction": true,
|
||||
// Risk threshold (0-100). If exceeded, bot pauses or alerts you
|
||||
"riskThreshold": 75
|
||||
},
|
||||
|
||||
"retryPolicy": {
|
||||
// Generic retry/backoff for transient failures
|
||||
"maxAttempts": 3,
|
||||
"baseDelay": 1000,
|
||||
"maxDelay": "30s",
|
||||
"multiplier": 2,
|
||||
"jitter": 0.2
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🌐 PROXY
|
||||
// ============================================================
|
||||
|
||||
"proxy": {
|
||||
// Control which outbound calls go through your proxy
|
||||
"proxyGoogleTrends": true,
|
||||
"proxyBingTerms": true
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 🔔 NOTIFICATIONS
|
||||
// ============================================================
|
||||
|
||||
"notifications": {
|
||||
// Live logs webhook (Discord or similar). URL = your webhook endpoint
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
// Optional: Customize webhook appearance
|
||||
"username": "Live Logs",
|
||||
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
|
||||
},
|
||||
// Rich end-of-run summary webhook (Discord or similar)
|
||||
"conclusionWebhook": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
// Optional: Customize webhook appearance (overrides webhook settings for conclusion messages)
|
||||
"username": "Microsoft Rewards",
|
||||
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
|
||||
},
|
||||
// NTFY push notifications (plain text)
|
||||
"ntfy": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
"topic": "rewards",
|
||||
"authToken": ""
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 📊 LOGGING & DIAGNOSTICS
|
||||
// ============================================================
|
||||
|
||||
"logging": {
|
||||
// Logging controls (see docs/config.md)
|
||||
// Filter out noisy log buckets locally and for webhook summaries
|
||||
"excludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
"webhookExcludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
// Email redaction toggle (true = secure, false = full emails)
|
||||
"redactEmails": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,17 +82,17 @@ export class Login {
|
||||
clearInterval(this.compromisedInterval)
|
||||
this.compromisedInterval = undefined
|
||||
}
|
||||
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
||||
this.currentTotpSecret = (totpSecret && totpSecret.trim()) || undefined
|
||||
|
||||
await page.goto('https://www.bing.com/rewards/dashboard')
|
||||
await this.disableFido(page)
|
||||
await page.waitForLoadState('domcontentloaded').catch(()=>{})
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => { })
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
await this.checkAccountLocked(page)
|
||||
|
||||
const already = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 8000 }).then(()=>true).catch(()=>false)
|
||||
const already = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 8000 }).then(() => true).catch(() => false)
|
||||
if (!already) {
|
||||
await this.performLoginFlow(page, email, password)
|
||||
} else {
|
||||
@@ -145,7 +145,7 @@ export class Login {
|
||||
const req: AxiosRequestConfig = { url: this.tokenUrl, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: form.toString() }
|
||||
const resp = await this.bot.axios.request(req)
|
||||
const data: OAuth = resp.data
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now()-start)/1000)}s`)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now() - start) / 1000)}s`)
|
||||
return data.access_token
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export class Login {
|
||||
await this.bot.utils.wait(500)
|
||||
await this.tryRecoveryMismatchCheck(page, email)
|
||||
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'recovery-mismatch') {
|
||||
this.bot.log(this.bot.isMobile,'LOGIN','Recovery mismatch detected – stopping before password entry','warn')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery mismatch detected – stopping before password entry', 'warn')
|
||||
return
|
||||
}
|
||||
// Try switching to password if a locale link is present (FR/EN)
|
||||
@@ -173,26 +173,26 @@ export class Login {
|
||||
|
||||
// --------------- Input Steps ---------------
|
||||
private async inputEmail(page: Page, email: string) {
|
||||
const field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null)
|
||||
const field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(() => null)
|
||||
if (!field) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present', 'warn'); return }
|
||||
const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(()=>null)
|
||||
const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(() => null)
|
||||
if (!prefilled) {
|
||||
await page.fill(SELECTORS.emailInput, '')
|
||||
await page.fill(SELECTORS.emailInput, email)
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled')
|
||||
}
|
||||
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
|
||||
if (next) { await next.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
|
||||
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
|
||||
if (next) { await next.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
|
||||
}
|
||||
|
||||
private async inputPasswordOr2FA(page: Page, password: string) {
|
||||
// Some flows require switching to password first
|
||||
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null)
|
||||
if (switchBtn) { await switchBtn.click().catch(()=>{}); await this.bot.utils.wait(1000) }
|
||||
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(() => null)
|
||||
if (switchBtn) { await switchBtn.click().catch(() => { }); await this.bot.utils.wait(1000) }
|
||||
|
||||
// Rare flow: list of methods -> choose password
|
||||
const passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(()=>null)
|
||||
const passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(() => null)
|
||||
if (!passwordField) {
|
||||
const blocked = await this.detectSignInBlocked(page)
|
||||
if (blocked) return
|
||||
@@ -204,13 +204,13 @@ export class Login {
|
||||
const otherWaysHandled = await this.handleOtherWaysToSignIn(page)
|
||||
if (otherWaysHandled) {
|
||||
// Try to find password field again after clicking "Other ways"
|
||||
const passwordFieldAfter = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(()=>null)
|
||||
const passwordFieldAfter = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(() => null)
|
||||
if (passwordFieldAfter) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field found after "Other ways" flow')
|
||||
await page.fill(SELECTORS.passwordInput, '')
|
||||
await page.fill(SELECTORS.passwordInput, password)
|
||||
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
|
||||
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
|
||||
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
|
||||
if (submit) { await submit.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -226,8 +226,8 @@ export class Login {
|
||||
|
||||
await page.fill(SELECTORS.passwordInput, '')
|
||||
await page.fill(SELECTORS.passwordInput, password)
|
||||
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
|
||||
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
|
||||
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
|
||||
if (submit) { await submit.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ export class Login {
|
||||
for (const selector of otherWaysSelectors) {
|
||||
const element = await page.waitForSelector(selector, { timeout: 1000 }).catch(() => null)
|
||||
if (element && await element.isVisible().catch(() => false)) {
|
||||
await element.click().catch(() => {})
|
||||
await element.click().catch(() => { })
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Other ways to sign in"')
|
||||
await this.bot.utils.wait(2000) // Wait for options to appear
|
||||
clicked = true
|
||||
@@ -273,7 +273,7 @@ export class Login {
|
||||
for (const selector of usePasswordSelectors) {
|
||||
const element = await page.waitForSelector(selector, { timeout: 1500 }).catch(() => null)
|
||||
if (element && await element.isVisible().catch(() => false)) {
|
||||
await element.click().catch(() => {})
|
||||
await element.click().catch(() => { })
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password"')
|
||||
await this.bot.utils.wait(2000) // Wait for password field to appear
|
||||
return true
|
||||
@@ -320,13 +320,13 @@ export class Login {
|
||||
if (this.bot.config.parallel) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Parallel mode: throttling authenticator push requests', 'log', 'yellow')
|
||||
for (let attempts = 0; attempts < 6; attempts++) { // max 6 minutes retry window
|
||||
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null)
|
||||
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(() => null)
|
||||
if (!resend) break
|
||||
await this.bot.utils.wait(60000)
|
||||
await resend.click().catch(()=>{})
|
||||
await resend.click().catch(() => { })
|
||||
}
|
||||
}
|
||||
await page.click('button[aria-describedby="confirmSendTitle"]').catch(()=>{})
|
||||
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
|
||||
await this.bot.utils.wait(1500)
|
||||
try {
|
||||
const el = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { timeout: 2000 })
|
||||
@@ -344,14 +344,14 @@ export class Login {
|
||||
return
|
||||
} catch {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired – refreshing')
|
||||
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null)
|
||||
if (retryBtn) await retryBtn.click().catch(()=>{})
|
||||
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(() => null)
|
||||
if (retryBtn) await retryBtn.click().catch(() => { })
|
||||
const refreshed = await this.fetchAuthenticatorNumber(page)
|
||||
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
|
||||
numberToPress = refreshed
|
||||
}
|
||||
}
|
||||
this.bot.log(this.bot.isMobile,'LOGIN','Authenticator approval loop exited (max cycles reached)','warn')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator approval loop exited (max cycles reached)', 'warn')
|
||||
}
|
||||
|
||||
private async handleSMSOrTotp(page: Page) {
|
||||
@@ -363,17 +363,17 @@ export class Login {
|
||||
await this.submitTotpCode(page, totpSelector)
|
||||
return
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
|
||||
// Manual prompt with periodic page check
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Waiting for user 2FA code (SMS / Email / App fallback)')
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
|
||||
|
||||
// Monitor page changes while waiting for user input
|
||||
let userInput: string | null = null
|
||||
let checkInterval: NodeJS.Timeout | null = null
|
||||
|
||||
|
||||
try {
|
||||
const inputPromise = new Promise<string>(res => {
|
||||
rl.question('Enter 2FA code:\n', ans => {
|
||||
@@ -395,11 +395,11 @@ export class Login {
|
||||
rl.close()
|
||||
userInput = 'skip' // Signal to skip submission
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}, 2000)
|
||||
|
||||
const code = await inputPromise
|
||||
|
||||
|
||||
if (code === 'skip' || userInput === 'skip') {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipping 2FA code submission (page progressed)')
|
||||
return
|
||||
@@ -411,7 +411,7 @@ export class Login {
|
||||
} finally {
|
||||
// Ensure cleanup happens even if errors occur
|
||||
if (checkInterval) clearInterval(checkInterval)
|
||||
try { rl.close() } catch {/* ignore */}
|
||||
try { rl.close() } catch {/* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ export class Login {
|
||||
try {
|
||||
const code = generateTOTP(this.currentTotpSecret!.trim())
|
||||
const input = page.locator(selector).first()
|
||||
if (!await input.isVisible().catch(()=>false)) {
|
||||
if (!await input.isVisible().catch(() => false)) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn')
|
||||
return
|
||||
}
|
||||
@@ -457,9 +457,9 @@ export class Login {
|
||||
// Use unified selector system
|
||||
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
|
||||
if (submit) {
|
||||
await submit.click().catch(()=>{})
|
||||
await submit.click().catch(() => { })
|
||||
} else {
|
||||
await page.keyboard.press('Enter').catch(()=>{})
|
||||
await page.keyboard.press('Enter').catch(() => { })
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
|
||||
} catch (error) {
|
||||
@@ -529,7 +529,7 @@ export class Login {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
await loc.click().catch(()=>{})
|
||||
await loc.click().catch(() => { })
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -560,7 +560,7 @@ export class Login {
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(()=>false)) {
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
return sel
|
||||
}
|
||||
}
|
||||
@@ -588,7 +588,7 @@ export class Login {
|
||||
if (!portalSelector) {
|
||||
try {
|
||||
await this.bot.browser.func.goHome(page)
|
||||
} catch {/* ignore fallback errors */}
|
||||
} catch {/* ignore fallback errors */ }
|
||||
|
||||
const fallbackSelector = await this.waitForRewardsRoot(page, 6000)
|
||||
if (!fallbackSelector) {
|
||||
@@ -605,60 +605,60 @@ export class Login {
|
||||
try {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing auth context')
|
||||
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F')
|
||||
for (let i=0;i<5;i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const u = new URL(page.url())
|
||||
if (u.hostname === 'www.bing.com' && u.pathname === '/') {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
const ok = await page.waitForSelector('#id_n', { timeout: 3000 }).then(()=>true).catch(()=>false)
|
||||
if (ok || this.bot.isMobile) { this.bot.log(this.bot.isMobile,'LOGIN-BING','Bing verification passed'); break }
|
||||
const ok = await page.waitForSelector('#id_n', { timeout: 3000 }).then(() => true).catch(() => false)
|
||||
if (ok || this.bot.isMobile) { this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification passed'); break }
|
||||
}
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification error: '+e, 'warn')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification error: ' + e, 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAccountLocked(page: Page) {
|
||||
const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(()=>true).catch(()=>false)
|
||||
if (locked) throw this.bot.log(this.bot.isMobile,'CHECK-LOCKED','Account locked by Microsoft (serviceAbuseLandingTitle)','error')
|
||||
const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(() => true).catch(() => false)
|
||||
if (locked) throw this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'Account locked by Microsoft (serviceAbuseLandingTitle)', 'error')
|
||||
}
|
||||
|
||||
// --------------- Passkey / Dialog Handling ---------------
|
||||
private async handlePasskeyPrompts(page: Page, context: 'main' | 'oauth') {
|
||||
let did = false
|
||||
// Video heuristic
|
||||
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null)
|
||||
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(() => null)
|
||||
if (biometric) {
|
||||
const btn = await page.$(SELECTORS.passkeySecondary)
|
||||
if (btn) { await btn.click().catch(()=>{}); did = true; this.logPasskeyOnce('video heuristic') }
|
||||
if (btn) { await btn.click().catch(() => { }); did = true; this.logPasskeyOnce('video heuristic') }
|
||||
}
|
||||
if (!did) {
|
||||
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(()=>null)
|
||||
const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(()=>null)
|
||||
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null)
|
||||
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(() => null)
|
||||
const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(() => null)
|
||||
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(() => null)
|
||||
const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
|
||||
const looksLike = /sign in faster|passkey|fingerprint|face|pin/i.test(title)
|
||||
if (looksLike && secBtn) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('title heuristic '+title) }
|
||||
if (looksLike && secBtn) { await secBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('title heuristic ' + title) }
|
||||
else if (!did && secBtn && primBtn) {
|
||||
const text = (await secBtn.textContent()||'').trim()
|
||||
if (/skip for now/i.test(text)) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('secondary button text') }
|
||||
const text = (await secBtn.textContent() || '').trim()
|
||||
if (/skip for now/i.test(text)) { await secBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('secondary button text') }
|
||||
}
|
||||
if (!did) {
|
||||
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now")]').first()
|
||||
if (await textBtn.isVisible().catch(()=>false)) { await textBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('text fallback') }
|
||||
if (await textBtn.isVisible().catch(() => false)) { await textBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('text fallback') }
|
||||
}
|
||||
if (!did) {
|
||||
const close = await page.$('#close-button')
|
||||
if (close) { await close.click().catch(()=>{}); did = true; this.logPasskeyOnce('close button') }
|
||||
if (close) { await close.click().catch(() => { }); did = true; this.logPasskeyOnce('close button') }
|
||||
}
|
||||
}
|
||||
|
||||
// KMSI prompt
|
||||
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null)
|
||||
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(() => null)
|
||||
if (kmsi) {
|
||||
const yes = await page.$(SELECTORS.passkeyPrimary)
|
||||
if (yes) { await yes.click().catch(()=>{}); did = true; this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') }
|
||||
if (yes) { await yes.click().catch(() => { }); did = true; this.bot.log(this.bot.isMobile, 'LOGIN-KMSI', 'Accepted KMSI prompt') }
|
||||
}
|
||||
|
||||
if (!did && context === 'main') {
|
||||
@@ -666,7 +666,7 @@ export class Login {
|
||||
const now = Date.now()
|
||||
if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) {
|
||||
this.lastNoPromptLog = now
|
||||
this.bot.log(this.bot.isMobile,'LOGIN-NO-PROMPT',`No dialogs (x${this.noPromptIterations})`)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-NO-PROMPT', `No dialogs (x${this.noPromptIterations})`)
|
||||
if (this.noPromptIterations > 50) this.noPromptIterations = 0
|
||||
}
|
||||
} else if (did) {
|
||||
@@ -677,7 +677,7 @@ export class Login {
|
||||
private logPasskeyOnce(reason: string) {
|
||||
if (this.passkeyHandled) return
|
||||
this.passkeyHandled = true
|
||||
this.bot.log(this.bot.isMobile,'LOGIN-PASSKEY',`Dismissed passkey prompt (${reason})`)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
|
||||
}
|
||||
|
||||
// --------------- Security Detection ---------------
|
||||
@@ -685,11 +685,11 @@ export class Login {
|
||||
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'sign-in-blocked') return true
|
||||
try {
|
||||
let text = ''
|
||||
for (const sel of ['[data-testid="title"]','h1','div[role="heading"]','div.text-title']) {
|
||||
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(()=>null)
|
||||
for (const sel of ['[data-testid="title"]', 'h1', 'div[role="heading"]', 'div.text-title']) {
|
||||
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(() => null)
|
||||
if (el) {
|
||||
const t = (await el.textContent()||'').trim()
|
||||
if (t && t.length < 300) text += ' '+t
|
||||
const t = (await el.textContent() || '').trim()
|
||||
if (t && t.length < 300) text += ' ' + t
|
||||
}
|
||||
}
|
||||
const lower = text.toLowerCase()
|
||||
@@ -697,57 +697,53 @@ export class Login {
|
||||
for (const p of SIGN_IN_BLOCK_PATTERNS) { if (p.re.test(lower)) { matched = p.label; break } }
|
||||
if (!matched) return false
|
||||
const email = this.bot.currentAccountEmail || 'unknown'
|
||||
const docsUrl = this.getDocsUrl('we-cant-sign-you-in')
|
||||
const incident: SecurityIncident = {
|
||||
kind: 'We can\'t sign you in (blocked)',
|
||||
account: email,
|
||||
details: [matched ? `Pattern: ${matched}` : 'Pattern: unknown'],
|
||||
next: ['Manual recovery required before continuing'],
|
||||
docsUrl
|
||||
next: ['Manual recovery required before continuing']
|
||||
}
|
||||
await this.sendIncidentAlert(incident,'warn')
|
||||
await this.sendIncidentAlert(incident, 'warn')
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'sign-in-blocked'
|
||||
this.startCompromisedInterval()
|
||||
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(()=>{})
|
||||
// Open security docs for immediate guidance (best-effort)
|
||||
await this.openDocsTab(page, docsUrl).catch(()=>{})
|
||||
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(() => { })
|
||||
return true
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
private async tryRecoveryMismatchCheck(page: Page, email: string) { try { await this.detectAndHandleRecoveryMismatch(page, email) } catch {/* ignore */} }
|
||||
private async tryRecoveryMismatchCheck(page: Page, email: string) { try { await this.detectAndHandleRecoveryMismatch(page, email) } catch {/* ignore */ } }
|
||||
private async detectAndHandleRecoveryMismatch(page: Page, email: string) {
|
||||
try {
|
||||
const recoveryEmail: string | undefined = this.bot.currentAccountRecoveryEmail
|
||||
if (!recoveryEmail || !/@/.test(recoveryEmail)) return
|
||||
const accountEmail = email
|
||||
const parseRef = (val: string) => { const [l,d] = val.split('@'); return { local: l||'', domain:(d||'').toLowerCase(), prefix2:(l||'').slice(0,2).toLowerCase() } }
|
||||
const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r=>r.domain && r.prefix2)
|
||||
const parseRef = (val: string) => { const [l, d] = val.split('@'); return { local: l || '', domain: (d || '').toLowerCase(), prefix2: (l || '').slice(0, 2).toLowerCase() } }
|
||||
const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r => r.domain && r.prefix2)
|
||||
if (refs.length === 0) return
|
||||
|
||||
const candidates: string[] = []
|
||||
// Direct selectors (Microsoft variants + French spans)
|
||||
const sel = '[data-testid="recoveryEmailHint"], #recoveryEmail, [id*="ProofEmail"], [id*="EmailProof"], [data-testid*="Email"], span:has(span.fui-Text)'
|
||||
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(()=>null)
|
||||
if (el) { const t = (await el.textContent()||'').trim(); if (t) candidates.push(t) }
|
||||
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(() => null)
|
||||
if (el) { const t = (await el.textContent() || '').trim(); if (t) candidates.push(t) }
|
||||
|
||||
// List items
|
||||
const li = page.locator('[role="listitem"], li')
|
||||
const liCount = await li.count().catch(()=>0)
|
||||
for (let i=0;i<liCount && i<12;i++) { const t = (await li.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && /@/.test(t)) candidates.push(t) }
|
||||
const liCount = await li.count().catch(() => 0)
|
||||
for (let i = 0; i < liCount && i < 12; i++) { const t = (await li.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && /@/.test(t)) candidates.push(t) }
|
||||
|
||||
// XPath generic masked patterns
|
||||
const xp = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]')
|
||||
const xpCount = await xp.count().catch(()=>0)
|
||||
for (let i=0;i<xpCount && i<12;i++) { const t = (await xp.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && t.length<300) candidates.push(t) }
|
||||
const xpCount = await xp.count().catch(() => 0)
|
||||
for (let i = 0; i < xpCount && i < 12; i++) { const t = (await xp.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && t.length < 300) candidates.push(t) }
|
||||
|
||||
// Normalize
|
||||
const seen = new Set<string>()
|
||||
const norm = (s:string)=>s.replace(/\s+/g,' ').trim()
|
||||
const uniq = candidates.map(norm).filter(t=>t && !seen.has(t) && seen.add(t))
|
||||
const norm = (s: string) => s.replace(/\s+/g, ' ').trim()
|
||||
const uniq = candidates.map(norm).filter(t => t && !seen.has(t) && seen.add(t))
|
||||
// Masked filter
|
||||
let masked = uniq.filter(t=>/@/.test(t) && /[*•]/.test(t))
|
||||
let masked = uniq.filter(t => /@/.test(t) && /[*•]/.test(t))
|
||||
|
||||
if (masked.length === 0) {
|
||||
// Fallback full HTML scan
|
||||
@@ -758,14 +754,14 @@ export class Login {
|
||||
const found = new Set<string>()
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = generic.exec(html)) !== null) found.add(m[0])
|
||||
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g,'').trim(); if (raw) found.add(raw) }
|
||||
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) }
|
||||
if (found.size > 0) masked = Array.from(found)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
if (masked.length === 0) return
|
||||
|
||||
// Prefer one mentioning email/adresse
|
||||
const preferred = masked.find(t=>/email|courriel|adresse|mail/i.test(t)) || masked[0]!
|
||||
const preferred = masked.find(t => /email|courriel|adresse|mail/i.test(t)) || masked[0]!
|
||||
// Extract the masked email: Microsoft sometimes shows only first 1 char (k*****@domain) or 2 chars (ko*****@domain).
|
||||
// We ONLY compare (1 or 2) leading visible alphanumeric chars + full domain (case-insensitive).
|
||||
// This avoids false positives when the displayed mask hides the 2nd char.
|
||||
@@ -776,15 +772,15 @@ export class Login {
|
||||
const use = m || loose
|
||||
const extracted = use ? use[0] : preferred
|
||||
const extractedLower = extracted.toLowerCase()
|
||||
let observedPrefix = ((use && use[1]) ? use[1] : '').toLowerCase()
|
||||
let observedDomain = ((use && use[2]) ? use[2] : '').toLowerCase()
|
||||
let observedPrefix = ((use && use[1]) ? use[1] : '').toLowerCase()
|
||||
let observedDomain = ((use && use[2]) ? use[2] : '').toLowerCase()
|
||||
if (!observedDomain && extractedLower.includes('@')) {
|
||||
const parts = extractedLower.split('@')
|
||||
observedDomain = parts[1] || ''
|
||||
}
|
||||
if (!observedPrefix && extractedLower.includes('@')) {
|
||||
const parts = extractedLower.split('@')
|
||||
observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi,'').slice(0,2)
|
||||
observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi, '').slice(0, 2)
|
||||
}
|
||||
|
||||
// Determine if any reference (recoveryEmail or accountEmail) matches observed mask logic
|
||||
@@ -798,55 +794,52 @@ export class Login {
|
||||
})
|
||||
|
||||
if (!matchRef) {
|
||||
const docsUrl = this.getDocsUrl('recovery-email-mismatch')
|
||||
const incident: SecurityIncident = {
|
||||
kind:'Recovery email mismatch',
|
||||
kind: 'Recovery email mismatch',
|
||||
account: email,
|
||||
details:[
|
||||
details: [
|
||||
`MaskedShown: ${preferred}`,
|
||||
`Extracted: ${extracted}`,
|
||||
`Observed => ${observedPrefix || '??'}**@${observedDomain || '??'}`,
|
||||
`Expected => ${refs.map(r=>`${r.prefix2}**@${r.domain}`).join(' OR ')}`
|
||||
`Expected => ${refs.map(r => `${r.prefix2}**@${r.domain}`).join(' OR ')}`
|
||||
],
|
||||
next:[
|
||||
next: [
|
||||
'Automation halted globally (standby engaged).',
|
||||
'Verify account security & recovery email in Microsoft settings.',
|
||||
'Update accounts.json if the change was legitimate before restart.'
|
||||
],
|
||||
docsUrl
|
||||
]
|
||||
}
|
||||
await this.sendIncidentAlert(incident,'critical')
|
||||
await this.sendIncidentAlert(incident, 'critical')
|
||||
this.bot.compromisedModeActive = true
|
||||
this.bot.compromisedReason = 'recovery-mismatch'
|
||||
this.startCompromisedInterval()
|
||||
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(()=>{})
|
||||
await this.openDocsTab(page, docsUrl).catch(()=>{})
|
||||
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(() => { })
|
||||
} else {
|
||||
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
|
||||
this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
|
||||
}
|
||||
} catch {/* non-fatal */}
|
||||
} catch {/* non-fatal */ }
|
||||
}
|
||||
|
||||
private async switchToPasswordLink(page: Page) {
|
||||
try {
|
||||
const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first()
|
||||
if (await link.isVisible().catch(()=>false)) {
|
||||
await link.click().catch(()=>{})
|
||||
if (await link.isVisible().catch(() => false)) {
|
||||
await link.click().catch(() => { })
|
||||
await this.bot.utils.wait(800)
|
||||
this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link')
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
|
||||
// --------------- Incident Helpers ---------------
|
||||
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn'|'critical'='warn') {
|
||||
const lines = [ `[Incident] ${incident.kind}`, `Account: ${incident.account}` ]
|
||||
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn' | 'critical' = 'warn') {
|
||||
const lines = [`[Incident] ${incident.kind}`, `Account: ${incident.account}`]
|
||||
if (incident.details?.length) lines.push(`Details: ${incident.details.join(' | ')}`)
|
||||
if (incident.next?.length) lines.push(`Next: ${incident.next.join(' -> ')}`)
|
||||
if (incident.docsUrl) lines.push(`Docs: ${incident.docsUrl}`)
|
||||
const level: 'warn'|'error' = severity === 'critical' ? 'error' : 'warn'
|
||||
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level)
|
||||
const level: 'warn' | 'error' = severity === 'critical' ? 'error' : 'warn'
|
||||
this.bot.log(this.bot.isMobile, 'SECURITY', lines.join(' | '), level)
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
const fields = [
|
||||
@@ -858,36 +851,18 @@ export class Login {
|
||||
await ConclusionWebhook(
|
||||
this.bot.config,
|
||||
`🔐 ${incident.kind}`,
|
||||
'_Security check by @Light_',
|
||||
'_Security check',
|
||||
fields,
|
||||
severity === 'critical' ? 0xFF0000 : 0xFFAA00
|
||||
)
|
||||
} catch {/* ignore */}
|
||||
}
|
||||
|
||||
private getDocsUrl(anchor?: string) {
|
||||
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/blob/v2/docs/security.md'
|
||||
const map: Record<string,string> = {
|
||||
'recovery-email-mismatch':'#recovery-email-mismatch',
|
||||
'we-cant-sign-you-in':'#we-cant-sign-you-in-blocked'
|
||||
}
|
||||
return anchor && map[anchor] ? `${base}${map[anchor]}` : base
|
||||
} catch {/* ignore */ }
|
||||
}
|
||||
|
||||
private startCompromisedInterval() {
|
||||
if (this.compromisedInterval) clearInterval(this.compromisedInterval)
|
||||
this.compromisedInterval = setInterval(()=>{
|
||||
try { this.bot.log(this.bot.isMobile,'SECURITY','Account in security standby. Review before proceeding. Security check by @Light','warn') } catch {/* ignore */}
|
||||
}, 5*60*1000)
|
||||
}
|
||||
|
||||
|
||||
private async openDocsTab(page: Page, url: string) {
|
||||
try {
|
||||
const ctx = page.context()
|
||||
const tab = await ctx.newPage()
|
||||
await tab.goto(url, { waitUntil: 'domcontentloaded' })
|
||||
} catch {/* ignore */}
|
||||
this.compromisedInterval = setInterval(() => {
|
||||
try { this.bot.log(this.bot.isMobile, 'SECURITY', 'Account in security standby. Review before proceeding.', 'warn') } catch {/* ignore */ }
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
// --------------- Infrastructure ---------------
|
||||
@@ -898,6 +873,6 @@ export class Login {
|
||||
body.isFidoSupported = false
|
||||
route.continue({ postData: JSON.stringify(body) })
|
||||
} catch { route.continue() }
|
||||
}).catch(()=>{})
|
||||
}).catch(() => { })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ export class DailyCheckIn extends Workers {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
'X-Rewards-Language': 'en',
|
||||
'X-Rewards-ismobile': 'true'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ export class ReadToEarn extends Workers {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
'X-Rewards-Language': 'en',
|
||||
'X-Rewards-ismobile': 'true'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
12
src/index.ts
12
src/index.ts
@@ -378,7 +378,7 @@ export class MicrosoftRewardsBot {
|
||||
}
|
||||
// If any account is flagged compromised, do NOT exit; keep the process alive so the browser stays open
|
||||
if (this.compromisedModeActive || this.globalStandby.active) {
|
||||
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done. Security check by @Light','warn','yellow')
|
||||
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done.','warn','yellow')
|
||||
const standbyInterval = setInterval(() => {
|
||||
log('main','SECURITY','Still in standby: session(s) held open for manual recovery / review...','warn','yellow')
|
||||
}, 5 * 60 * 1000)
|
||||
@@ -467,13 +467,13 @@ export class MicrosoftRewardsBot {
|
||||
if (this.compromisedModeActive) {
|
||||
// User wants the page to remain open for manual recovery. Do not proceed to tasks.
|
||||
const reason = this.compromisedReason || 'security-issue'
|
||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}.`, 'warn', 'yellow')
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🔐 Security Alert (Post-Login)',
|
||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving browser open; skipping tasks\n\n_Security check by @Light_`,
|
||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving browser open; skipping tasks`,
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
@@ -571,13 +571,13 @@ export class MicrosoftRewardsBot {
|
||||
await this.login.login(this.homePage, account.email, account.password, account.totp)
|
||||
if (this.compromisedModeActive) {
|
||||
const reason = this.compromisedReason || 'security-issue'
|
||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
|
||||
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}.`, 'warn', 'yellow')
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🔐 Security Alert (Mobile)',
|
||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving mobile browser open; skipping tasks\n\n_Security check by @Light_`,
|
||||
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving mobile browser open; skipping tasks`,
|
||||
undefined,
|
||||
0xFFAA00
|
||||
)
|
||||
@@ -790,7 +790,7 @@ export class MicrosoftRewardsBot {
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🚨 Global Security Standby Engaged',
|
||||
`@everyone\n\n**Account:** ${email}\n**Reason:** ${reason}\n**Action:** Pausing all further accounts. We will not proceed until this is resolved.\n\n_Security check by @Light_`,
|
||||
`@everyone\n\n**Account:** ${email}\n**Reason:** ${reason}\n**Action:** Pausing all further accounts. We will not proceed until this is resolved.`,
|
||||
undefined,
|
||||
DISCORD.COLOR_RED
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface Config {
|
||||
webhook: ConfigWebhook;
|
||||
conclusionWebhook?: ConfigWebhook; // Optional secondary webhook for final summary
|
||||
ntfy: ConfigNtfy;
|
||||
passesPerRun?: number;
|
||||
vacation?: ConfigVacation; // Optional monthly contiguous off-days
|
||||
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
|
||||
riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction
|
||||
|
||||
@@ -29,12 +29,6 @@ interface DiscordEmbed {
|
||||
}
|
||||
}
|
||||
|
||||
interface WebhookPayload {
|
||||
username: string
|
||||
avatar_url: string
|
||||
embeds: DiscordEmbed[]
|
||||
}
|
||||
|
||||
interface AccountSummary {
|
||||
email: string
|
||||
totalCollected: number
|
||||
@@ -89,26 +83,20 @@ export async function ConclusionWebhook(
|
||||
embed.fields = fields
|
||||
}
|
||||
|
||||
// Use custom webhook settings if provided, otherwise fall back to defaults
|
||||
const webhookUsername = config.webhook?.username || config.conclusionWebhook?.username || 'Microsoft Rewards'
|
||||
const webhookAvatarUrl = config.webhook?.avatarUrl || config.conclusionWebhook?.avatarUrl || DISCORD.AVATAR_URL
|
||||
|
||||
const payload: WebhookPayload = {
|
||||
username: webhookUsername,
|
||||
avatar_url: webhookAvatarUrl,
|
||||
embeds: [embed]
|
||||
}
|
||||
|
||||
const postWebhook = async (url: string, label: string) => {
|
||||
const maxAttempts = 3
|
||||
let lastError: unknown = null
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 15000
|
||||
})
|
||||
await axios.post(url,
|
||||
{
|
||||
embeds: [embed]
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 15000
|
||||
})
|
||||
log('main', 'WEBHOOK', `${label} notification sent successfully (attempt ${attempt})`)
|
||||
return
|
||||
} catch (error) {
|
||||
@@ -160,7 +148,7 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
|
||||
|
||||
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
|
||||
if (minutes > 0) return `${minutes}m ${seconds}s`
|
||||
return `${seconds}s`
|
||||
@@ -199,14 +187,14 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
|
||||
// Build global statistics field
|
||||
const globalStats = [
|
||||
`**💎 Total Points Earned**`,
|
||||
'**💎 Total Points Earned**',
|
||||
`\`${data.totalInitial.toLocaleString()}\` → \`${data.totalEnd.toLocaleString()}\` **(+${data.totalCollected.toLocaleString()})**`,
|
||||
'',
|
||||
`**📊 Accounts Processed**`,
|
||||
'**📊 Accounts Processed**',
|
||||
`✅ Success: **${data.successes}** | ⚠️ Errors: **${data.accountsWithErrors}** | 🚫 Banned: **${data.accountsBanned}**`,
|
||||
`Total: **${data.totalAccounts}** ${data.totalAccounts === 1 ? 'account' : 'accounts'}`,
|
||||
'',
|
||||
`**⚡ Performance**`,
|
||||
'**⚡ Performance**',
|
||||
`Average: **${data.avgPointsPerAccount}pts/account** in **${formatDuration(data.avgDuration)}**`,
|
||||
`Total Runtime: **${formatDuration(data.totalDuration)}**`
|
||||
].join('\n')
|
||||
@@ -215,33 +203,33 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
const accountFields: DiscordField[] = []
|
||||
const maxAccountsPerField = 5
|
||||
const accountChunks: AccountSummary[][] = []
|
||||
|
||||
|
||||
for (let i = 0; i < data.summaries.length; i += maxAccountsPerField) {
|
||||
accountChunks.push(data.summaries.slice(i, i + maxAccountsPerField))
|
||||
}
|
||||
|
||||
accountChunks.forEach((chunk, chunkIndex) => {
|
||||
const accountLines: string[] = []
|
||||
|
||||
|
||||
chunk.forEach((acc) => {
|
||||
const statusIcon = acc.banned?.status ? '🚫' : (acc.errors.length > 0 ? '⚠️' : '✅')
|
||||
const emailShort = acc.email.length > 25 ? acc.email.substring(0, 22) + '...' : acc.email
|
||||
|
||||
|
||||
accountLines.push(`${statusIcon} **${emailShort}**`)
|
||||
accountLines.push(`└ Points: **+${acc.totalCollected}** (🖥️ ${acc.desktopCollected} • 📱 ${acc.mobileCollected})`)
|
||||
accountLines.push(`└ Duration: ${formatDuration(acc.durationMs)}`)
|
||||
|
||||
|
||||
if (acc.banned?.status) {
|
||||
accountLines.push(`└ 🚫 **Banned:** ${acc.banned.reason || 'Account suspended'}`)
|
||||
} else if (acc.errors.length > 0) {
|
||||
const errorPreview = acc.errors.slice(0, 1).join(', ')
|
||||
accountLines.push(`└ ⚠️ **Error:** ${errorPreview.length > 50 ? errorPreview.substring(0, 47) + '...' : errorPreview}`)
|
||||
}
|
||||
|
||||
|
||||
accountLines.push('') // Empty line between accounts
|
||||
})
|
||||
|
||||
const fieldName = accountChunks.length > 1
|
||||
const fieldName = accountChunks.length > 1
|
||||
? `📈 Account Details (${chunkIndex + 1}/${accountChunks.length})`
|
||||
: '📈 Account Details'
|
||||
|
||||
@@ -295,15 +283,6 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
})
|
||||
}
|
||||
|
||||
// Use custom webhook settings
|
||||
const webhookUsername = config.conclusionWebhook?.username || config.webhook?.username || 'Microsoft Rewards'
|
||||
const webhookAvatarUrl = config.conclusionWebhook?.avatarUrl || config.webhook?.avatarUrl || DISCORD.AVATAR_URL
|
||||
|
||||
const payload: WebhookPayload = {
|
||||
username: webhookUsername,
|
||||
avatar_url: webhookAvatarUrl,
|
||||
embeds
|
||||
}
|
||||
|
||||
const postWebhook = async (url: string, label: string) => {
|
||||
const maxAttempts = 3
|
||||
@@ -311,7 +290,9 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
await axios.post(url, payload, {
|
||||
await axios.post(url, {
|
||||
embeds: embeds
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 15000
|
||||
})
|
||||
@@ -339,13 +320,13 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
|
||||
// Optional NTFY notification (simplified summary)
|
||||
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
|
||||
const message = [
|
||||
`🎯 Microsoft Rewards Summary`,
|
||||
'🎯 Microsoft Rewards Summary',
|
||||
`Status: ${statusText}`,
|
||||
`Points: ${data.totalInitial} → ${data.totalEnd} (+${data.totalCollected})`,
|
||||
`Accounts: ${data.successes}/${data.totalAccounts} successful`,
|
||||
`Duration: ${formatDuration(data.totalDuration)}`
|
||||
].join('\n')
|
||||
|
||||
|
||||
const ntfyType = embedColor === DISCORD.COLOR_RED ? 'error' : embedColor === DISCORD.COLOR_ORANGE ? 'warn' : 'log'
|
||||
|
||||
try {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ValidationResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigValidator performs intelligent validation of config.jsonc and accounts.json
|
||||
* ConfigValidator performs intelligent validation of config.json and accounts.json
|
||||
* before execution to catch common mistakes, conflicts, and security issues.
|
||||
*/
|
||||
export class ConfigValidator {
|
||||
@@ -400,7 +400,6 @@ export class ConfigValidator {
|
||||
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)
|
||||
|
||||
122
src/util/Load.ts
122
src/util/Load.ts
@@ -10,62 +10,6 @@ import { Config, ConfigSaveFingerprint } from '../interface/Config'
|
||||
let configCache: Config
|
||||
let configSourcePath = ''
|
||||
|
||||
// Basic JSON comment stripper (supports // line and /* block */ comments while preserving strings)
|
||||
function stripJsonComments(input: string): string {
|
||||
let out = ''
|
||||
let inString = false
|
||||
let stringChar = ''
|
||||
let inLine = false
|
||||
let inBlock = false
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const ch = input[i]!
|
||||
const next = input[i + 1]
|
||||
if (inLine) {
|
||||
if (ch === '\n' || ch === '\r') {
|
||||
inLine = false
|
||||
out += ch
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (inBlock) {
|
||||
if (ch === '*' && next === '/') {
|
||||
inBlock = false
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (inString) {
|
||||
out += ch
|
||||
if (ch === '\\') { // escape next char
|
||||
i++
|
||||
if (i < input.length) out += input[i]
|
||||
continue
|
||||
}
|
||||
if (ch === stringChar) {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (ch === '"' || ch === '\'') {
|
||||
inString = true
|
||||
stringChar = ch
|
||||
out += ch
|
||||
continue
|
||||
}
|
||||
if (ch === '/' && next === '/') {
|
||||
inLine = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (ch === '/' && next === '*') {
|
||||
inBlock = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
out += ch
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
|
||||
function normalizeConfig(raw: unknown): Config {
|
||||
@@ -79,7 +23,6 @@ function normalizeConfig(raw: unknown): Config {
|
||||
const parallel = n.execution?.parallel ?? n.parallel ?? false
|
||||
const runOnZeroPoints = n.execution?.runOnZeroPoints ?? n.runOnZeroPoints ?? false
|
||||
const clusters = n.execution?.clusters ?? n.clusters ?? 1
|
||||
const passesPerRun = n.execution?.passesPerRun ?? n.passesPerRun
|
||||
|
||||
// Search
|
||||
const useLocalQueries = n.search?.useLocalQueries ?? n.searchOnBingLocalQueries ?? false
|
||||
@@ -166,17 +109,16 @@ function normalizeConfig(raw: unknown): Config {
|
||||
searchOnBingLocalQueries: !!useLocalQueries,
|
||||
globalTimeout,
|
||||
searchSettings,
|
||||
humanization: n.humanization,
|
||||
humanization: n.humanization,
|
||||
retryPolicy: n.retryPolicy,
|
||||
jobState: n.jobState,
|
||||
logExcludeFunc,
|
||||
webhookLogExcludeFunc,
|
||||
logging, // retain full logging object for live webhook usage
|
||||
proxy: n.proxy ?? { proxyGoogleTrends: true, proxyBingTerms: true },
|
||||
logging, // retain full logging object for live webhook usage
|
||||
proxy: n.proxy ?? { proxyGoogleTrends: true, proxyBingTerms: true },
|
||||
webhook,
|
||||
conclusionWebhook,
|
||||
ntfy,
|
||||
passesPerRun: passesPerRun,
|
||||
vacation: n.vacation,
|
||||
crashRecovery: n.crashRecovery || {}
|
||||
}
|
||||
@@ -196,41 +138,35 @@ export function loadAccounts(): Account[] {
|
||||
const envJson = process.env.ACCOUNTS_JSON
|
||||
const envFile = process.env.ACCOUNTS_FILE
|
||||
|
||||
let raw: string | undefined
|
||||
let json: string | undefined
|
||||
if (envJson && envJson.trim().startsWith('[')) {
|
||||
raw = envJson
|
||||
json = envJson
|
||||
} else if (envFile && envFile.trim()) {
|
||||
const full = path.isAbsolute(envFile) ? envFile : path.join(process.cwd(), envFile)
|
||||
if (!fs.existsSync(full)) {
|
||||
throw new Error(`ACCOUNTS_FILE not found: ${full}`)
|
||||
}
|
||||
raw = fs.readFileSync(full, 'utf-8')
|
||||
json = fs.readFileSync(full, 'utf-8')
|
||||
} else {
|
||||
// Try multiple locations to support both root mounts and dist mounts
|
||||
// Support both .json and .jsonc extensions
|
||||
// Support both .json and .json extensions
|
||||
const candidates = [
|
||||
path.join(__dirname, '../', file), // root/accounts.json (preferred)
|
||||
path.join(__dirname, '../', file + 'c'), // root/accounts.jsonc
|
||||
path.join(__dirname, '../src', file), // fallback: file kept inside src/
|
||||
path.join(__dirname, '../src', file + 'c'), // src/accounts.jsonc
|
||||
path.join(process.cwd(), file), // cwd override
|
||||
path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc
|
||||
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
|
||||
path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc
|
||||
path.join(__dirname, file), // dist/accounts.json (legacy)
|
||||
path.join(__dirname, file + 'c') // dist/accounts.jsonc
|
||||
path.join(__dirname, '../', file),
|
||||
path.join(__dirname, '../src', file),
|
||||
path.join(process.cwd(), file),
|
||||
path.join(process.cwd(), 'src', file),
|
||||
path.join(__dirname, file)
|
||||
]
|
||||
let chosen: string | null = null
|
||||
for (const p of candidates) {
|
||||
try { if (fs.existsSync(p)) { chosen = p; break } } catch { /* ignore */ }
|
||||
}
|
||||
if (!chosen) throw new Error(`accounts file not found in: ${candidates.join(' | ')}`)
|
||||
raw = fs.readFileSync(chosen, 'utf-8')
|
||||
json = fs.readFileSync(chosen, 'utf-8')
|
||||
}
|
||||
|
||||
// Support comments in accounts file (same as config)
|
||||
const cleaned = stripJsonComments(raw)
|
||||
const parsedUnknown = JSON.parse(cleaned)
|
||||
const parsedUnknown = JSON.parse(json)
|
||||
// Accept either a root array or an object with an `accounts` array, ignore `_note`
|
||||
const parsed = Array.isArray(parsedUnknown) ? parsedUnknown : (parsedUnknown && typeof parsedUnknown === 'object' && Array.isArray((parsedUnknown as { accounts?: unknown }).accounts) ? (parsedUnknown as { accounts: unknown[] }).accounts : null)
|
||||
if (!Array.isArray(parsed)) throw new Error('accounts must be an array')
|
||||
@@ -257,8 +193,8 @@ export function loadConfig(): Config {
|
||||
return configCache
|
||||
}
|
||||
|
||||
// Resolve configuration file from common locations (supports .jsonc and .json)
|
||||
const names = ['config.jsonc', 'config.json']
|
||||
// Resolve configuration file from common locations
|
||||
const names = ['config.json']
|
||||
const bases = [
|
||||
path.join(__dirname, '../'), // dist root when compiled
|
||||
path.join(__dirname, '../src'), // fallback: running dist but config still in src
|
||||
@@ -272,19 +208,19 @@ export function loadConfig(): Config {
|
||||
candidates.push(path.join(base, name))
|
||||
}
|
||||
}
|
||||
let cfgPath: string | null = null
|
||||
let cfgPath: string | null = null
|
||||
for (const p of candidates) {
|
||||
try { if (fs.existsSync(p)) { cfgPath = p; break } } catch { /* ignore */ }
|
||||
}
|
||||
if (!cfgPath) throw new Error(`config.json not found in: ${candidates.join(' | ')}`)
|
||||
const config = fs.readFileSync(cfgPath, 'utf-8')
|
||||
const text = config.replace(/^\uFEFF/, '')
|
||||
const raw = JSON.parse(stripJsonComments(text))
|
||||
const normalized = normalizeConfig(raw)
|
||||
configCache = normalized // Set as cache
|
||||
configSourcePath = cfgPath
|
||||
const json = config.replace(/^\uFEFF/, '')
|
||||
const raw = JSON.parse(json)
|
||||
const normalized = normalizeConfig(raw)
|
||||
configCache = normalized // Set as cache
|
||||
configSourcePath = cfgPath
|
||||
|
||||
return normalized
|
||||
return normalized
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
@@ -357,12 +293,12 @@ export async function saveFingerprintData(sessionPath: string, email: string, is
|
||||
await fs.promises.mkdir(sessionDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Save fingerprint to files (write both legacy and corrected names for compatibility)
|
||||
const legacy = path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
|
||||
const correct = path.join(sessionDir, `${isMobile ? 'mobile_fingerprint' : 'desktop_fingerprint'}.json`)
|
||||
const payload = JSON.stringify(fingerprint)
|
||||
await fs.promises.writeFile(correct, payload)
|
||||
try { await fs.promises.writeFile(legacy, payload) } catch { /* ignore */ }
|
||||
// Save fingerprint to files (write both legacy and corrected names for compatibility)
|
||||
const legacy = path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
|
||||
const correct = path.join(sessionDir, `${isMobile ? 'mobile_fingerprint' : 'desktop_fingerprint'}.json`)
|
||||
const payload = JSON.stringify(fingerprint)
|
||||
await fs.promises.writeFile(correct, payload)
|
||||
try { await fs.promises.writeFile(legacy, payload) } catch { /* ignore */ }
|
||||
|
||||
return sessionDir
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Ntfy } from './Ntfy'
|
||||
import { loadConfig } from './Load'
|
||||
import { DISCORD } from '../constants'
|
||||
|
||||
const DEFAULT_LIVE_LOG_USERNAME = 'MS Rewards - Live Logs'
|
||||
|
||||
type WebhookBuffer = {
|
||||
lines: string[]
|
||||
sending: boolean
|
||||
@@ -19,7 +17,7 @@ const webhookBuffers = new Map<string, WebhookBuffer>()
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
const BUFFER_MAX_AGE_MS = 3600000 // 1 hour
|
||||
|
||||
|
||||
for (const [url, buf] of webhookBuffers.entries()) {
|
||||
if (!buf.sending && buf.lines.length === 0) {
|
||||
const lastActivity = (buf as unknown as { lastActivity?: number }).lastActivity || 0
|
||||
@@ -44,12 +42,7 @@ function getBuffer(url: string): WebhookBuffer {
|
||||
async function sendBatch(url: string, buf: WebhookBuffer) {
|
||||
if (buf.sending) return
|
||||
buf.sending = true
|
||||
|
||||
// Load config to get webhook settings
|
||||
const configData = loadConfig()
|
||||
const webhookUsername = configData.webhook?.username || DEFAULT_LIVE_LOG_USERNAME
|
||||
const webhookAvatarUrl = configData.webhook?.avatarUrl || DISCORD.AVATAR_URL
|
||||
|
||||
|
||||
while (buf.lines.length > 0) {
|
||||
const chunk: string[] = []
|
||||
let currentLength = 0
|
||||
@@ -69,8 +62,6 @@ async function sendBatch(url: string, buf: WebhookBuffer) {
|
||||
|
||||
// Enhanced webhook payload with embed, username and avatar
|
||||
const payload = {
|
||||
username: webhookUsername,
|
||||
avatar_url: webhookAvatarUrl,
|
||||
embeds: [{
|
||||
description: `\`\`\`\n${content}\n\`\`\``,
|
||||
color: determineColorFromContent(content),
|
||||
@@ -143,13 +134,13 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
|
||||
const currentTime = new Date().toLocaleString()
|
||||
const platformText = isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP'
|
||||
|
||||
|
||||
// Clean string for notifications (no chalk, structured)
|
||||
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
|
||||
const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg
|
||||
const shouldRedact = !!loggingCfg.redactEmails
|
||||
const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => {
|
||||
const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}`
|
||||
const [u, d] = m.split('@'); return `${(u || '').slice(0, 2)}***@${d || ''}`
|
||||
}) : s
|
||||
const cleanStr = redact(`[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`)
|
||||
|
||||
@@ -160,7 +151,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
message.toLowerCase().includes('press the number'),
|
||||
message.toLowerCase().includes('no points to earn')
|
||||
],
|
||||
error: [],
|
||||
error: [],
|
||||
warn: [
|
||||
message.toLowerCase().includes('aborting'),
|
||||
message.toLowerCase().includes('didn\'t gain')
|
||||
@@ -179,11 +170,11 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
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]'],
|
||||
@@ -198,7 +189,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
[/browser/i, '[BROWSER]'],
|
||||
[/main/i, '[MAIN]']
|
||||
]
|
||||
|
||||
|
||||
let icon = ''
|
||||
for (const [pattern, symbol] of iconMap) {
|
||||
if (pattern.test(titleLower) || pattern.test(msgLower)) {
|
||||
@@ -206,9 +197,9 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const iconPart = icon ? icon + ' ' : ''
|
||||
|
||||
|
||||
const formattedStr = [
|
||||
chalk.gray(`[${currentTime}]`),
|
||||
chalk.gray(`[${process.pid}]`),
|
||||
|
||||
Reference in New Issue
Block a user