mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-10 02:16:18 +00:00
Small update to be deployed quickly. (#358)
* chore: Update TypeScript configuration and add @types/node as a dev dependency * feat: Add unified cross-platform setup script for easier configuration and installation * docs: Revise README for improved setup instructions and clarity * feat: Enhance setup scripts with improved prerequisite checks and user prompts * feat: Refactor setup scripts and enhance browser handling with automatic Playwright installation
This commit is contained in:
271
README.md
271
README.md
@@ -1,122 +1,191 @@
|
||||
# Microsoft-Rewards-Script
|
||||
Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Playwright.
|
||||
Automated Microsoft Rewards script built with TypeScript, Cheerio and Playwright.
|
||||
|
||||
Under development, however mainly for personal use!
|
||||
|
||||
## How to setup ##
|
||||
1. Download or clone source code
|
||||
2. Change `accounts.example.json` to `accounts.json` and add your account details
|
||||
3. Change `config.json` to your liking
|
||||
4. Either go the nix or non-nix route
|
||||
---
|
||||
|
||||
### How to setup (not with nix) ###
|
||||
5. Run `npm i` to install the packages
|
||||
6. Run `npm run build` to build the script
|
||||
7. Run `npm run start` to start the built script
|
||||
## 🚀 Quick Setup (Recommended)
|
||||
|
||||
### How to setup (with nix) ##
|
||||
5. Get [Nix](https://nixos.org/)
|
||||
6. Run `./run.sh`
|
||||
7. That's it!
|
||||
**The easiest way to get started - just download and run!**
|
||||
|
||||
## Notes ##
|
||||
- If you end the script without closing the browser window first (only with headless as false), you'll be left with hanging chrome instances using resources. Use taskmanager to kill these or use the included `npm run kill-chrome-win` script. (Windows)
|
||||
- If you automate this script, set it to run at least 2 times a day to make sure it picked up all tasks, set `"runOnZeroPoints": false` so it doesn't run when no points are found.
|
||||
1. **Download or clone** the source code
|
||||
2. **Run the setup script:**
|
||||
|
||||
## Docker (Experimental) ##
|
||||
### **Before Starting**
|
||||
**Windows:** Double-click `setup/setup.bat` or run it from command line
|
||||
|
||||
- If you had previously built and run the script locally, **remove** the `/node_modules` and `/dist` folders from your `Microsoft-Rewards-Script` directory.
|
||||
- If you had used Docker with an older version of the script (e.g., 1.4), **remove** any persistently saved `config.json` and session folders. Old `accounts.json` files can be reused.
|
||||
**Linux/macOS/WSL:** `bash setup/setup.sh`
|
||||
|
||||
### **Setup the Source Files**
|
||||
**Alternative (any platform):** `npm run setup`
|
||||
|
||||
1. **Download the Source Code**
|
||||
3. **Follow the prompts:** The setup script will automatically:
|
||||
- Rename `accounts.example.json` to `accounts.json`
|
||||
- Ask you to enter your Microsoft account credentials
|
||||
- Remind you to review configuration options in `config.json`
|
||||
- Install all dependencies (`npm install`)
|
||||
- Build the project (`npm run build`)
|
||||
- Optionally start the script immediately
|
||||
|
||||
2. **Update `accounts.json`**
|
||||
**That's it!** The setup script handles everything for you.
|
||||
|
||||
3. **Edit `config.json`,** ensuring "headless": true, other settings are up to your preference
|
||||
---
|
||||
|
||||
### **Customize the `compose.yaml` File**
|
||||
## ⚙️ Advanced Setup Options
|
||||
|
||||
A basic docker `compose.yaml` is provided. Follow these steps to configure and run the container:
|
||||
### Nix Users
|
||||
1. Get [Nix](https://nixos.org/)
|
||||
2. Run `./run.sh`
|
||||
3. Done!
|
||||
|
||||
1. **Set Your Timezone:** Adjust the `TZ` variable to ensure correct scheduling.
|
||||
3. **Customize the Schedule:**
|
||||
- Modify `CRON_SCHEDULE` to set run times. Use [crontab.guru](https://crontab.guru) for help.
|
||||
- **Note:** The container adds 5–50 minutes of random variability to each scheduled start time. This can be optionally disabled or customized in the compose file.
|
||||
4. **(Optional) Run on Startup:**
|
||||
- Set `RUN_ON_START=true` to execute the script immediately when the container starts.
|
||||
5. **Start the Container:** Run `docker compose up -d` to build and launch.
|
||||
6. **Monitor Logs:** Use `docker logs microsoft-rewards-script` to view script execution and to retrieve 'passwordless' login codes.
|
||||
### Manual Setup (Troubleshooting)
|
||||
If the automatic setup script doesn't work for your environment:
|
||||
|
||||
1. Manually rename `src/accounts.example.json` to `src/accounts.json`
|
||||
2. Add your Microsoft account details to `accounts.json`
|
||||
3. Customize `src/config.json` to your preferences
|
||||
4. Install dependencies: `npm install`
|
||||
5. Build the project: `npm run build`
|
||||
6. Start the script: `npm run start`---
|
||||
|
||||
## Config ##
|
||||
## 🐳 Docker Setup (Experimental)
|
||||
|
||||
For automated scheduling and containerized deployment.
|
||||
|
||||
### Before Starting
|
||||
- Remove `/node_modules` and `/dist` folders if you previously built locally
|
||||
- Remove old Docker volumes if upgrading from version 1.4 or earlier
|
||||
- Old `accounts.json` files can be reused
|
||||
|
||||
### Quick Docker Setup
|
||||
1. **Download source code** and configure `accounts.json`
|
||||
2. **Edit `config.json`** - ensure `"headless": true`
|
||||
3. **Customize `compose.yaml`:**
|
||||
- Set your timezone (`TZ` variable)
|
||||
- Configure schedule (`CRON_SCHEDULE`) - use [crontab.guru](https://crontab.guru) for help
|
||||
- Optional: Set `RUN_ON_START=true` for immediate execution
|
||||
4. **Start container:** `docker compose up -d`
|
||||
5. **Monitor logs:** `docker logs microsoft-rewards-script`
|
||||
|
||||
**Note:** The container adds 5–50 minutes random delay to scheduled runs for more natural behavior.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Usage Notes
|
||||
|
||||
- **Browser Instances:** If you stop the script without closing browser windows (headless=false), use Task Manager or `npm run kill-chrome-win` to clean up
|
||||
- **Automation Scheduling:** Run at least twice daily, set `"runOnZeroPoints": false` to skip when no points available
|
||||
- **Multiple Accounts:** The script supports clustering - configure `clusters` in `config.json`
|
||||
|
||||
---
|
||||
## ⚙️ Configuration Reference
|
||||
|
||||
Customize behavior by editing `src/config.json`:
|
||||
|
||||
### Core Settings
|
||||
| Setting | Description | Default |
|
||||
| :------------- |:-------------| :-----|
|
||||
| baseURL | MS Rewards page | `https://rewards.bing.com` |
|
||||
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
|
||||
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
|
||||
| parallel | If you want mobile and desktop tasks to run parallel or sequential| `true` |
|
||||
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) |
|
||||
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
|
||||
| saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
|
||||
| saveFingerprint.desktop | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
|
||||
| workers.doDailySet | Complete daily set items | `true` |
|
||||
| workers.doMorePromotions | Complete promotional items | `true` |
|
||||
| workers.doPunchCards | Complete punchcards | `true` |
|
||||
| workers.doDesktopSearch | Complete daily desktop searches | `true` |
|
||||
| workers.doMobileSearch | Complete daily mobile searches | `true` |
|
||||
| workers.doDailyCheckIn | Complete daily check-in activity | `true` |
|
||||
| workers.doReadToEarn | Complete read to earn activity | `true` |
|
||||
| searchOnBingLocalQueries | Complete the activity "search on Bing" using the `queries.json` or fetched from this repo | `false` (Will fetch from this repo) |
|
||||
| globalTimeout | The length before the action gets timeout | `30s` |
|
||||
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
|
||||
| searchSettings.scrollRandomResults | Scroll randomly in search results | `true` |
|
||||
| searchSettings.clickRandomResults | Visit random website from search result| `true` |
|
||||
| searchSettings.searchDelay | Minimum and maximum time in milliseconds between search queries | `min: 3min` `max: 5min` |
|
||||
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `2` |
|
||||
| logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` |
|
||||
| webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` |
|
||||
| proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
|
||||
| proxy.proxyBingTerms | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
|
||||
| webhook.enabled | Enable or disable your set webhook | `false` |
|
||||
| webhook.url | Your Discord webhook URL | `null` |
|
||||
| conclusionWebhook.enabled | Enable or disable the final summary dedicated webhook | `false` |
|
||||
| conclusionWebhook.url | Discord webhook URL used ONLY for the end summary | `null` |
|
||||
|---------|-------------|---------|
|
||||
| `baseURL` | Microsoft Rewards page URL | `https://rewards.bing.com` |
|
||||
| `sessionPath` | Session/fingerprint storage location | `sessions` |
|
||||
| `headless` | Run browser in background | `false` (visible) |
|
||||
| `parallel` | Run mobile/desktop tasks simultaneously | `true` |
|
||||
| `runOnZeroPoints` | Continue when no points available | `false` |
|
||||
| `clusters` | Number of concurrent account instances | `1` |
|
||||
|
||||
## Features ##
|
||||
- [x] Multi-Account Support
|
||||
- [x] Session Storing
|
||||
- [x] 2FA Support
|
||||
- [x] Passwordless Support
|
||||
- [x] Headless Support
|
||||
- [x] Discord Webhook Support
|
||||
- [x] Final Summary Webhook (dedicated optional)
|
||||
- [x] Desktop Searches
|
||||
- [x] Configurable Tasks
|
||||
- [x] Microsoft Edge Searches
|
||||
- [x] Mobile Searches
|
||||
- [x] Emulated Scrolling Support
|
||||
- [x] Emulated Link Clicking Support
|
||||
- [x] Geo Locale Search Queries
|
||||
- [x] Completing Daily Set
|
||||
- [x] Completing More Promotions
|
||||
- [x] Solving Quiz (10 point variant)
|
||||
- [x] Solving Quiz (30-40 point variant)
|
||||
- [x] Completing Click Rewards
|
||||
- [x] Completing Polls
|
||||
- [x] Completing Punchcards
|
||||
- [x] Solving This Or That Quiz (Random)
|
||||
- [x] Solving ABC Quiz
|
||||
- [x] Completing Daily Check In
|
||||
- [x] Completing Read To Earn
|
||||
- [x] Clustering Support
|
||||
- [x] Proxy Support
|
||||
- [x] Docker Support (experimental)
|
||||
- [x] Automatic scheduling (via Docker)
|
||||
### Fingerprint Settings
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `saveFingerprint.mobile` | Reuse mobile browser fingerprint | `false` |
|
||||
| `saveFingerprint.desktop` | Reuse desktop browser fingerprint | `false` |
|
||||
|
||||
## Disclaimer ##
|
||||
Your account may be at risk of getting banned or suspended using this script, you've been warned!
|
||||
<br />
|
||||
Use this script at your own risk!
|
||||
### Task Settings
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `workers.doDailySet` | Complete daily set activities | `true` |
|
||||
| `workers.doMorePromotions` | Complete promotional offers | `true` |
|
||||
| `workers.doPunchCards` | Complete punchcard activities | `true` |
|
||||
| `workers.doDesktopSearch` | Perform desktop searches | `true` |
|
||||
| `workers.doMobileSearch` | Perform mobile searches | `true` |
|
||||
| `workers.doDailyCheckIn` | Complete daily check-in | `true` |
|
||||
| `workers.doReadToEarn` | Complete read-to-earn activities | `true` |
|
||||
|
||||
### Search Settings
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `searchOnBingLocalQueries` | Use local queries vs. fetched | `false` |
|
||||
| `searchSettings.useGeoLocaleQueries` | Generate location-based queries | `false` |
|
||||
| `searchSettings.scrollRandomResults` | Randomly scroll search results | `true` |
|
||||
| `searchSettings.clickRandomResults` | Click random result links | `true` |
|
||||
| `searchSettings.searchDelay` | Delay between searches (min/max) | `3-5 minutes` |
|
||||
| `searchSettings.retryMobileSearchAmount` | Mobile search retry attempts | `2` |
|
||||
|
||||
### Advanced Settings
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `globalTimeout` | Action timeout duration | `30s` |
|
||||
| `logExcludeFunc` | Functions to exclude from logs | `SEARCH-CLOSE-TABS` |
|
||||
| `webhookLogExcludeFunc` | Functions to exclude from webhooks | `SEARCH-CLOSE-TABS` |
|
||||
| `proxy.proxyGoogleTrends` | Proxy Google Trends requests | `true` |
|
||||
| `proxy.proxyBingTerms` | Proxy Bing Terms requests | `true` |
|
||||
|
||||
### Webhook Settings
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `webhook.enabled` | Enable Discord notifications | `false` |
|
||||
| `webhook.url` | Discord webhook URL | `null` |
|
||||
| `conclusionWebhook.enabled` | Enable summary-only webhook | `false` |
|
||||
| `conclusionWebhook.url` | Summary webhook URL | `null` |
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
**Account Management:**
|
||||
- ✅ Multi-Account Support
|
||||
- ✅ Session Storage & Persistence
|
||||
- ✅ 2FA Support
|
||||
- ✅ Passwordless Login Support
|
||||
|
||||
**Automation & Control:**
|
||||
- ✅ Headless Browser Operation
|
||||
- ✅ Clustering Support (Multiple accounts simultaneously)
|
||||
- ✅ Configurable Task Selection
|
||||
- ✅ Proxy Support
|
||||
- ✅ Automatic Scheduling (Docker)
|
||||
|
||||
**Search & Activities:**
|
||||
- ✅ Desktop & Mobile Searches
|
||||
- ✅ Microsoft Edge Search Simulation
|
||||
- ✅ Geo-Located Search Queries
|
||||
- ✅ Emulated Scrolling & Link Clicking
|
||||
- ✅ Daily Set Completion
|
||||
- ✅ Promotional Activities
|
||||
- ✅ Punchcard Completion
|
||||
- ✅ Daily Check-in
|
||||
- ✅ Read to Earn Activities
|
||||
|
||||
**Quiz & Interactive Content:**
|
||||
- ✅ Quiz Solving (10 & 30-40 point variants)
|
||||
- ✅ This Or That Quiz (Random answers)
|
||||
- ✅ ABC Quiz Solving
|
||||
- ✅ Poll Completion
|
||||
- ✅ Click Rewards
|
||||
|
||||
**Notifications & Monitoring:**
|
||||
- ✅ Discord Webhook Integration
|
||||
- ✅ Dedicated Summary Webhook
|
||||
- ✅ Comprehensive Logging
|
||||
- ✅ Docker Support with Monitoring
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
**Use at your own risk!** Your Microsoft Rewards account may be suspended or banned when using automation scripts.
|
||||
|
||||
This script is provided for educational purposes. The authors are not responsible for any account actions taken by Microsoft.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This project is primarily for personal use but contributions are welcome. Please ensure any changes maintain compatibility with the existing configuration system.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"start": "node ./dist/index.js",
|
||||
"ts-start": "ts-node ./src/index.ts",
|
||||
"dev": "ts-node ./src/index.ts -dev",
|
||||
"setup": "node ./setup/setup.mjs",
|
||||
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
|
||||
"create-docker": "docker build -t microsoft-rewards-script-docker ."
|
||||
},
|
||||
@@ -27,6 +28,7 @@
|
||||
"author": "Netsky",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
22
setup/setup.bat
Normal file
22
setup/setup.bat
Normal file
@@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
setlocal
|
||||
REM Lightweight wrapper to run setup.mjs without prereq detection (Windows)
|
||||
REM Assumes Node is already installed and available in PATH.
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
set SETUP_FILE=%SCRIPT_DIR%setup.mjs
|
||||
|
||||
if not exist "%SETUP_FILE%" (
|
||||
echo [ERROR] setup.mjs not found next to this batch file.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Running setup script...
|
||||
node "%SETUP_FILE%"
|
||||
set EXITCODE=%ERRORLEVEL%
|
||||
echo.
|
||||
echo Setup finished with exit code %EXITCODE%.
|
||||
echo Press Enter to close.
|
||||
pause >NUL
|
||||
exit /b %EXITCODE%
|
||||
171
setup/setup.mjs
Normal file
171
setup/setup.mjs
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Unified cross-platform setup script for Microsoft Rewards Script.
|
||||
* Handles:
|
||||
* - Renaming accounts.example.json -> accounts.json (idempotent)
|
||||
* - Prompt loop to confirm passwords added
|
||||
* - Inform about config.json and conclusionWebhook
|
||||
* - Run npm install + npm run build
|
||||
* - Optional start
|
||||
*/
|
||||
|
||||
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 = parent of this setup 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.json');
|
||||
if (fs.existsSync(accounts)) {
|
||||
log('accounts.json already exists - skipping rename.');
|
||||
return;
|
||||
}
|
||||
if (fs.existsSync(example)) {
|
||||
log('Renaming accounts.example.json to accounts.json...');
|
||||
fs.renameSync(example, accounts);
|
||||
} else {
|
||||
warn('Neither accounts.json nor accounts.example.json 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() {
|
||||
// Keep asking until user says yes
|
||||
for (;;) {
|
||||
const ans = (await prompt('Have you entered your passwords in accounts.json? (yes/no) : ')).toLowerCase();
|
||||
if (['yes', 'y'].includes(ans)) break;
|
||||
if (['no', 'n'].includes(ans)) {
|
||||
log('Please enter your passwords in accounts.json and save the file (Ctrl+S), then answer yes.');
|
||||
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('\nYou can now review config.json (same folder) to adjust settings such as conclusionWebhook.');
|
||||
log('(How to enable it is documented in the repository README.)\n');
|
||||
await ensureNpmAvailable();
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
|
||||
await installPlaywrightBrowsers();
|
||||
const start = (await prompt('Do you want to start the program now? (yes/no) : ')).toLowerCase();
|
||||
if (['yes', 'y'].includes(start)) {
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']);
|
||||
} else {
|
||||
log('Finished setup without starting.');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
41
setup/setup.sh
Normal file
41
setup/setup.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Wrapper to run unified Node setup script (setup/setup.mjs) regardless of CWD.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SETUP_FILE="${SCRIPT_DIR}/setup.mjs"
|
||||
|
||||
echo "=== Prerequisite Check ==="
|
||||
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
NODE_VERSION="$(node -v 2>/dev/null || true)"
|
||||
echo "Node detected: ${NODE_VERSION}"
|
||||
else
|
||||
echo "[WARN] Node.js not detected."
|
||||
echo " Install (Linux): use your package manager (e.g. 'sudo apt install nodejs npm' or install from nodejs.org for latest)."
|
||||
fi
|
||||
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
GIT_VERSION="$(git --version 2>/dev/null || true)"
|
||||
echo "Git detected: ${GIT_VERSION}"
|
||||
else
|
||||
echo "[WARN] Git not detected."
|
||||
echo " Install (Linux): e.g. 'sudo apt install git' (or your distro equivalent)."
|
||||
fi
|
||||
|
||||
if [ -z "${NODE_VERSION:-}" ]; then
|
||||
read -r -p "Continue anyway? (yes/no) : " CONTINUE
|
||||
case "${CONTINUE,,}" in
|
||||
yes|y) ;;
|
||||
*) echo "Aborting. Install prerequisites then re-run."; exit 1;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ ! -f "${SETUP_FILE}" ]; then
|
||||
echo "[ERROR] setup.mjs not found at ${SETUP_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Running setup script ==="
|
||||
exec node "${SETUP_FILE}"
|
||||
@@ -25,7 +25,18 @@ class Browser {
|
||||
}
|
||||
|
||||
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
|
||||
const browser = await playwright.chromium.launch({
|
||||
// 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') as any
|
||||
execSync('npx playwright install chromium', { stdio: 'ignore' })
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
let browser: any
|
||||
try {
|
||||
browser = await playwright.chromium.launch({
|
||||
//channel: 'msedge', // Uses Edge instead of chrome
|
||||
headless: this.bot.config.headless,
|
||||
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
||||
@@ -38,6 +49,16 @@ class Browser {
|
||||
'--ignore-ssl-errors'
|
||||
]
|
||||
})
|
||||
} catch (e: any) {
|
||||
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: "npx playwright install chromium" (or set AUTO_INSTALL_BROWSERS=1 to auto attempt).', 'error')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
|
||||
|
||||
|
||||
42
src/index.ts
42
src/index.ts
@@ -170,14 +170,28 @@ export class MicrosoftRewardsBot {
|
||||
const errors: string[] = []
|
||||
|
||||
this.axios = new Axios(account.proxy)
|
||||
const verbose = process.env.DEBUG_REWARDS_VERBOSE === '1'
|
||||
const formatFullErr = (label: string, e: any) => {
|
||||
const base = shortErr(e)
|
||||
if (verbose && e instanceof Error) {
|
||||
return `${label}:${base} :: ${e.stack?.split('\n').slice(0,4).join(' | ')}`
|
||||
}
|
||||
return `${label}:${base}`
|
||||
}
|
||||
|
||||
if (this.config.parallel) {
|
||||
const mobileInstance = new MicrosoftRewardsBot(true)
|
||||
mobileInstance.axios = this.axios
|
||||
// Run both and capture results
|
||||
const [desktopResult, mobileResult] = await Promise.all([
|
||||
this.Desktop(account).catch(e => { errors.push(`desktop:${shortErr(e)}`); return null }),
|
||||
mobileInstance.Mobile(account).catch(e => { errors.push(`mobile:${shortErr(e)}`); return null })
|
||||
])
|
||||
// Run both and capture results with detailed logging
|
||||
const desktopPromise = this.Desktop(account).catch(e => {
|
||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
||||
errors.push(formatFullErr('desktop', e)); return null
|
||||
})
|
||||
const mobilePromise = mobileInstance.Mobile(account).catch(e => {
|
||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
||||
errors.push(formatFullErr('mobile', e)); return null
|
||||
})
|
||||
const [desktopResult, mobileResult] = await Promise.all([desktopPromise, mobilePromise])
|
||||
if (desktopResult) {
|
||||
desktopInitial = desktopResult.initialPoints
|
||||
desktopCollected = desktopResult.collectedPoints
|
||||
@@ -188,14 +202,20 @@ export class MicrosoftRewardsBot {
|
||||
}
|
||||
} else {
|
||||
this.isMobile = false
|
||||
const desktopResult = await this.Desktop(account).catch(e => { errors.push(`desktop:${shortErr(e)}`); return null })
|
||||
const desktopResult = await this.Desktop(account).catch(e => {
|
||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
||||
errors.push(formatFullErr('desktop', e)); return null
|
||||
})
|
||||
if (desktopResult) {
|
||||
desktopInitial = desktopResult.initialPoints
|
||||
desktopCollected = desktopResult.collectedPoints
|
||||
}
|
||||
|
||||
this.isMobile = true
|
||||
const mobileResult = await this.Mobile(account).catch(e => { errors.push(`mobile:${shortErr(e)}`); return null })
|
||||
const mobileResult = await this.Mobile(account).catch(e => {
|
||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
||||
errors.push(formatFullErr('mobile', e)); return null
|
||||
})
|
||||
if (mobileResult) {
|
||||
mobileInitial = mobileResult.initialPoints
|
||||
mobileCollected = mobileResult.collectedPoints
|
||||
@@ -221,6 +241,12 @@ export class MicrosoftRewardsBot {
|
||||
}
|
||||
|
||||
log(this.isMobile, 'MAIN-PRIMARY', 'Completed tasks for ALL accounts', 'log', 'green')
|
||||
// Extra diagnostic summary when verbose
|
||||
if (process.env.DEBUG_REWARDS_VERBOSE === '1') {
|
||||
for (const summary of this.accountSummaries) {
|
||||
log('main','SUMMARY-DEBUG',`Account ${summary.email} collected D:${summary.desktopCollected} M:${summary.mobileCollected} TOTAL:${summary.totalCollected} ERRORS:${summary.errors.length ? summary.errors.join(';') : 'none'}`)
|
||||
}
|
||||
}
|
||||
// If in worker mode (clusters>1) send summaries to primary
|
||||
if (this.config.clusters > 1 && !cluster.isPrimary) {
|
||||
if (process.send) {
|
||||
@@ -235,6 +261,7 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
// Desktop
|
||||
async Desktop(account: Account) {
|
||||
log(false,'FLOW','Desktop() invoked')
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
@@ -311,6 +338,7 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
// Mobile
|
||||
async Mobile(account: Account) {
|
||||
log(true,'FLOW','Mobile() invoked')
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
|
||||
78
src/types/node-shim.d.ts
vendored
78
src/types/node-shim.d.ts
vendored
@@ -1,78 +0,0 @@
|
||||
// Fallback in case @types/node not installed yet; ensures Process/stubs to reduce red squiggles.
|
||||
// Prefer installing @types/node for full types.
|
||||
|
||||
interface ProcessEnv { [key: string]: string | undefined }
|
||||
|
||||
interface Process {
|
||||
pid: number
|
||||
exit(code?: number): never
|
||||
send?(message: any): void
|
||||
on(event: string, listener: (...args: any[]) => void): any
|
||||
stdin: { on(event: string, listener: (...args: any[]) => void): any }
|
||||
stdout: { write(chunk: any): boolean }
|
||||
env: ProcessEnv
|
||||
}
|
||||
|
||||
declare var process: Process
|
||||
|
||||
// Minimal axios module declaration
|
||||
declare module 'axios' {
|
||||
export interface AxiosRequestConfig { [key: string]: any }
|
||||
export interface AxiosResponse<T = any> { data: T }
|
||||
export interface AxiosInstance {
|
||||
defaults: any
|
||||
request<T=any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
||||
}
|
||||
export interface AxiosStatic {
|
||||
(config: AxiosRequestConfig): Promise<AxiosResponse>
|
||||
request<T=any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>
|
||||
create(config?: AxiosRequestConfig): AxiosInstance
|
||||
}
|
||||
const axios: AxiosStatic
|
||||
export default axios
|
||||
}
|
||||
|
||||
// Minimal readline
|
||||
declare module 'readline' {
|
||||
export interface Interface { question(query: string, cb: (answer: string)=>void): void; close(): void }
|
||||
export function createInterface(opts: any): Interface
|
||||
export default {} as any
|
||||
}
|
||||
|
||||
// Minimal crypto
|
||||
declare module 'crypto' {
|
||||
export function randomBytes(size: number): { toString(encoding: string): string }
|
||||
}
|
||||
|
||||
// Minimal os module
|
||||
declare module 'os' {
|
||||
export function platform(): string
|
||||
}
|
||||
|
||||
// Minimal cheerio subset
|
||||
declare module 'cheerio' {
|
||||
export interface CheerioAPI {
|
||||
(selector: any): any
|
||||
load(html: string): CheerioAPI
|
||||
text(): string
|
||||
}
|
||||
export function load(html: string): CheerioAPI
|
||||
}
|
||||
|
||||
declare module 'cluster' {
|
||||
import { EventEmitter } from 'events'
|
||||
interface WorkerLike extends EventEmitter {
|
||||
id: number
|
||||
process: { pid: number }
|
||||
send?(message: any): void
|
||||
on(event: 'message', listener: (msg: any) => void): any
|
||||
}
|
||||
interface Cluster extends EventEmitter {
|
||||
isPrimary: boolean
|
||||
fork(env?: NodeJS.ProcessEnv): WorkerLike
|
||||
workers?: Record<string, WorkerLike>
|
||||
on(event: 'exit', listener: (worker: WorkerLike, code: number) => void): any
|
||||
}
|
||||
const cluster: Cluster
|
||||
export default cluster
|
||||
}
|
||||
57
src/types/rebrowser-playwright.d.ts
vendored
57
src/types/rebrowser-playwright.d.ts
vendored
@@ -1,57 +0,0 @@
|
||||
// Minimal module declaration to silence TS complaints if upstream types not found.
|
||||
// You should replace with actual types if the package provides them.
|
||||
|
||||
// Basic playwright stubs (only what we currently need). Replace with real @types if available.
|
||||
declare module 'playwright' {
|
||||
export interface Cookie { name: string; value: string; domain?: string; path?: string; expires?: number; httpOnly?: boolean; secure?: boolean; sameSite?: 'Lax'|'Strict'|'None' }
|
||||
export interface BrowserContext {
|
||||
newPage(): Promise<Page>
|
||||
setDefaultTimeout(timeout: number): void
|
||||
addCookies(cookies: Cookie[]): Promise<void>
|
||||
cookies(): Promise<Cookie[]>
|
||||
pages(): Page[]
|
||||
close(): Promise<void>
|
||||
}
|
||||
export interface Browser {
|
||||
newPage(): Promise<Page>
|
||||
context(): BrowserContext
|
||||
close(): Promise<void>
|
||||
pages?(): Page[]
|
||||
}
|
||||
export interface Keyboard {
|
||||
type(text: string): Promise<any>
|
||||
press(key: string): Promise<any>
|
||||
down(key: string): Promise<any>
|
||||
up(key: string): Promise<any>
|
||||
}
|
||||
export interface Locator {
|
||||
first(): Locator
|
||||
click(opts?: any): Promise<any>
|
||||
isVisible(opts?: any): Promise<boolean>
|
||||
nth(index: number): Locator
|
||||
}
|
||||
export interface Page {
|
||||
goto(url: string, opts?: any): Promise<any>
|
||||
waitForLoadState(state?: string, opts?: any): Promise<any>
|
||||
waitForSelector(selector: string, opts?: any): Promise<any>
|
||||
fill(selector: string, value: string): Promise<any>
|
||||
keyboard: Keyboard
|
||||
click(selector: string, opts?: any): Promise<any>
|
||||
close(): Promise<any>
|
||||
url(): string
|
||||
route(match: string, handler: any): Promise<any>
|
||||
locator(selector: string): Locator
|
||||
$: (selector: string) => Promise<any>
|
||||
context(): BrowserContext
|
||||
reload(opts?: any): Promise<any>
|
||||
evaluate<R=any>(pageFunction: any, arg?: any): Promise<R>
|
||||
content(): Promise<string>
|
||||
waitForTimeout(timeout: number): Promise<void>
|
||||
}
|
||||
export interface ChromiumType { launch(opts?: any): Promise<Browser> }
|
||||
export const chromium: ChromiumType
|
||||
}
|
||||
|
||||
declare module 'rebrowser-playwright' {
|
||||
export * from 'playwright'
|
||||
}
|
||||
25
src/types/shims-node.d.ts
vendored
25
src/types/shims-node.d.ts
vendored
@@ -1,25 +0,0 @@
|
||||
// Minimal shims to silence TypeScript errors in environments without @types/node
|
||||
// If possible, install @types/node instead for full typing.
|
||||
|
||||
declare const __dirname: string
|
||||
|
||||
declare namespace NodeJS { interface Process { pid: number; send?: (msg: any) => void; exit(code?: number): void; } }
|
||||
|
||||
declare const process: NodeJS.Process
|
||||
|
||||
declare module 'cluster' {
|
||||
interface Worker { process: { pid: number }; on(event: 'message', cb: (msg: any) => void): void }
|
||||
const isPrimary: boolean
|
||||
function fork(): Worker
|
||||
function on(event: 'exit', cb: (worker: Worker, code: number) => void): void
|
||||
export { isPrimary, fork, on, Worker }
|
||||
export default { isPrimary, fork, on }
|
||||
}
|
||||
|
||||
declare module 'fs' { const x: any; export = x }
|
||||
|
||||
declare module 'path' { const x: any; export = x }
|
||||
|
||||
// Do NOT redeclare 'Page' to avoid erasing actual Playwright types if present.
|
||||
// If types are missing, install: npm i -D @types/node
|
||||
|
||||
@@ -40,8 +40,9 @@
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution":"node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "types": ["node"], // Removed explicit requirement; using local shims. Install @types/node for full typings.
|
||||
"typeRoots": ["./src/types", "./node_modules/@types"],
|
||||
"types": ["node"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
|
||||
Reference in New Issue
Block a user