Premier commit
This commit is contained in:
40
.eslintrc.js
Normal file
40
.eslintrc.js
Normal file
@@ -0,0 +1,40 @@
|
||||
module.exports = {
|
||||
'env': {
|
||||
'es2021': true,
|
||||
'node': true
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
'parser': '@typescript-eslint/parser',
|
||||
'parserOptions': {
|
||||
'ecmaVersion': 12,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
'plugins': [
|
||||
'@typescript-eslint'
|
||||
],
|
||||
'rules': {
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any':
|
||||
['warn', {
|
||||
fixToUnknown: true // This line is optional and only relevant if you are using TypeScript
|
||||
}],
|
||||
'comma-dangle': 'off',
|
||||
'@typescript-eslint/comma-dangle': 'error',
|
||||
'prefer-arrow-callback': 'error'
|
||||
// Add any other rules you want to enforce here
|
||||
}
|
||||
}
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.sh text eol=lf
|
||||
*.template text eol=lf
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
sessions/
|
||||
dist/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
accounts.json
|
||||
notes
|
||||
accounts.dev.json
|
||||
accounts.main.json
|
||||
.DS_Store
|
||||
47
Dockerfile
Normal file
47
Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Use an official Node.js runtime as a base image
|
||||
FROM node:18
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/microsoft-rewards-script
|
||||
|
||||
# Install necessary dependencies for Playwright and cron
|
||||
RUN apt-get update && apt-get install -y \
|
||||
jq \
|
||||
cron \
|
||||
gettext-base \
|
||||
xvfb \
|
||||
libgbm-dev \
|
||||
libnss3 \
|
||||
libasound2 \
|
||||
libxss1 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
tzdata \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy all files to the working directory
|
||||
COPY . .
|
||||
|
||||
# Install dependencies, set permissions, and build the script
|
||||
RUN npm install && \
|
||||
chmod -R 755 /usr/src/microsoft-rewards-script/node_modules && \
|
||||
npm run pre-build && \
|
||||
npm run build
|
||||
|
||||
# Copy cron file to cron directory
|
||||
COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
|
||||
|
||||
# Create the log file to be able to run tail
|
||||
RUN touch /var/log/cron.log
|
||||
|
||||
# Define the command to run your application with cron optionally
|
||||
CMD ["sh", "-c", "echo \"$TZ\" > /etc/timezone && \
|
||||
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
|
||||
dpkg-reconfigure -f noninteractive tzdata && \
|
||||
envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && \
|
||||
chmod 0644 /etc/cron.d/microsoft-rewards-cron && \
|
||||
crontab /etc/cron.d/microsoft-rewards-cron && \
|
||||
cron -f & \
|
||||
([ \"$RUN_ON_START\" = \"true\" ] && npm start) && \
|
||||
tail -f /var/log/cron.log"]
|
||||
247
README.md
Normal file
247
README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Microsoft Rewards Automation Script
|
||||
|
||||

|
||||
[](https://msrewards-script.com)
|
||||
|
||||
A sophisticated, feature-rich automation script for Microsoft Rewards, built with TypeScript, Cheerio, and Playwright. This script automates the process of earning Microsoft Rewards points by completing various activities including searches, quizzes, polls, and more.
|
||||
|
||||
**Project Website:** [https://msrewards-script.com](https://msrewards-script.com)
|
||||
|
||||
## Table of Contents
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Config File Options](#config-file-options)
|
||||
- [Account Setup](#account-setup)
|
||||
- [Usage](#usage)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Docker Usage](#docker-usage)
|
||||
- [Advanced Features](#advanced-features)
|
||||
- [Proxy Support](#proxy-support)
|
||||
- [Session Management](#session-management)
|
||||
- [Webhook Integration](#webhook-integration)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Disclaimer](#disclaimer)
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Multi-Account Support**: Run multiple Microsoft accounts sequentially or in parallel
|
||||
- ✅ **Authentication Support**: Works with standard login, 2FA, and passwordless authentication
|
||||
- ✅ **Search Automation**: Completes desktop and mobile search requirements
|
||||
- ✅ **Activity Completion**:
|
||||
- Daily set tasks
|
||||
- Promotional activities
|
||||
- Various quiz types (Multiple choice, This-or-That, ABC)
|
||||
- Polls and click rewards
|
||||
- Punchcards
|
||||
- Daily check-in and Read-to-Earn activities
|
||||
- ✅ **Advanced Browsing Simulation**:
|
||||
- Realistic scrolling behavior
|
||||
- Random result clicking
|
||||
- Configurable delays between actions
|
||||
- ✅ **Infrastructure**:
|
||||
- Session persistence between runs
|
||||
- Docker support with scheduling
|
||||
- Proxy configuration options
|
||||
- Discord webhook integration for notifications
|
||||
- Clustering for handling multiple accounts efficiently
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 14+ and npm
|
||||
- Docker (optional, for containerized deployment)
|
||||
- Microsoft Rewards account(s)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/Microsoft-Rewards-Script.git
|
||||
cd Microsoft-Rewards-Script
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Create configuration files:
|
||||
```bash
|
||||
cp accounts.example.json accounts.json
|
||||
```
|
||||
|
||||
4. Edit the `accounts.json` file with your Microsoft account details.
|
||||
|
||||
5. Build the project:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Config File Options
|
||||
|
||||
| Setting | Description | Default |
|
||||
| :------------------------------------ | :----------------------------------------------------------- | :-------------------------------------- |
|
||||
| `baseURL` | Microsoft Rewards homepage URL | `https://rewards.bing.com` |
|
||||
| `sessionPath` | Directory to store sessions and fingerprints | `sessions` |
|
||||
| `headless` | Run browsers invisibly in the background | `false` (browser is visible) |
|
||||
| `parallel` | Run mobile and desktop tasks simultaneously | `true` |
|
||||
| `runOnZeroPoints` | Run script even when no points can be earned | `false` |
|
||||
| `clusters` | Number of simultaneous browser instances | `1` |
|
||||
| `saveFingerprint.mobile` | Reuse the same fingerprint for mobile browsing | `false` |
|
||||
| `saveFingerprint.desktop` | Reuse the same fingerprint for desktop browsing | `false` |
|
||||
| `workers.doDailySet` | Complete daily set activities | `true` |
|
||||
| `workers.doMorePromotions` | Complete promotional activities | `true` |
|
||||
| `workers.doPunchCards` | Complete punchcard activities | `true` |
|
||||
| `workers.doDesktopSearch` | Complete desktop search requirements | `true` |
|
||||
| `workers.doMobileSearch` | Complete mobile search requirements | `true` |
|
||||
| `workers.doDailyCheckIn` | Complete daily check-in activity | `true` |
|
||||
| `workers.doReadToEarn` | Complete read-to-earn activity | `true` |
|
||||
| `searchOnBingLocalQueries` | Use local queries instead of fetching from repo | `false` |
|
||||
| `globalTimeout` | Default timeout for operations | `30s` |
|
||||
| `searchSettings.useGeoLocaleQueries` | Generate queries based on geo-location | `false` (uses EN-US) |
|
||||
| `searchSettings.scrollRandomResults` | Randomly scroll in search results | `true` |
|
||||
| `searchSettings.clickRandomResults` | Visit random websites from search results | `true` |
|
||||
| `searchSettings.searchDelay` | Minimum and maximum delay between searches | `min: 3min, max: 5min` |
|
||||
| `searchSettings.retryMobileSearchAmount` | Retry attempts for mobile searches | `2` |
|
||||
| `logExcludeFunc` | Functions to exclude from logs | `SEARCH-CLOSE-TABS` |
|
||||
| `webhookLogExcludeFunc` | Functions to exclude from webhook logs | `SEARCH-CLOSE-TABS` |
|
||||
| `proxy.proxyGoogleTrends` | Route Google Trends requests through proxy | `true` |
|
||||
| `proxy.proxyBingTerms` | Route Bing terms requests through proxy | `true` |
|
||||
| `webhook.enabled` | Enable Discord webhook notifications | `false` |
|
||||
| `webhook.url` | Discord webhook URL | `null` |
|
||||
|
||||
### Account Setup
|
||||
|
||||
Edit the `accounts.json` file with your Microsoft account information:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"username": "your-email@example.com",
|
||||
"password": "your-password",
|
||||
"proxy": {
|
||||
"url": "http://proxy-url.com",
|
||||
"port": "8080",
|
||||
"username": "proxy-username",
|
||||
"password": "proxy-password"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Note: The proxy configuration is optional.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
After installing and configuring the script, you can run it with:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Docker Usage
|
||||
|
||||
#### Prerequisites
|
||||
- Docker and Docker Compose installed
|
||||
- Basic understanding of Docker and containers
|
||||
|
||||
#### Setup Steps
|
||||
|
||||
1. **Prepare Your Environment**
|
||||
|
||||
- If you've run the script locally before, remove the `/node_modules` and `/dist` folders
|
||||
- If upgrading from an older Docker version, remove any persistent `config.json` and session folders
|
||||
|
||||
2. **Configure Docker**
|
||||
|
||||
Edit the `compose.yaml` file to customize:
|
||||
- Timezone via the `TZ` environment variable
|
||||
- Persistent storage mappings for configuration and session data
|
||||
- Cron schedule for automatic execution using `CRON_SCHEDULE` (use [crontab.guru](https://crontab.guru) for help)
|
||||
- Set `RUN_ON_START=true` to execute immediately on container start
|
||||
|
||||
3. **Start the Container**
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. **Monitor Execution**
|
||||
|
||||
```bash
|
||||
docker logs microsoft-rewards-script
|
||||
```
|
||||
|
||||
This command shows execution logs and any passwordless login codes.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Proxy Support
|
||||
|
||||
The script supports using proxies for individual accounts. Configure in each account entry:
|
||||
|
||||
```json
|
||||
"proxy": {
|
||||
"url": "http://your-proxy-server.com",
|
||||
"port": "8080",
|
||||
"username": "proxy-username",
|
||||
"password": "proxy-password"
|
||||
}
|
||||
```
|
||||
|
||||
You can also configure which requests should be proxied in the `config.json` file.
|
||||
|
||||
### Session Management
|
||||
|
||||
Sessions are stored in the directory specified by `sessionPath` in your config. This helps:
|
||||
- Reduce login frequency
|
||||
- Preserve authentication across script runs
|
||||
- Maintain consistent browser fingerprints if enabled
|
||||
|
||||
### Webhook Integration
|
||||
|
||||
Configure Discord webhook notifications by updating the webhook settings in `config.json`:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://discord.com/api/webhooks/your-webhook-url"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Lingering Chrome Instances**: If you terminate the script without properly closing browser windows, Chrome instances may remain running. To clean up:
|
||||
- Windows: Run `npm run kill-chrome-win`
|
||||
- Linux/Mac: Use `pkill -f "chrome"`
|
||||
|
||||
- **Login Issues**: If you experience login problems:
|
||||
- Delete session data in your configured sessions folder
|
||||
- Verify account credentials
|
||||
- Check if your account requires additional security verification
|
||||
|
||||
- **Zero Points Error**: If the script exits with a "0 points can be earned" message:
|
||||
- Set `"runOnZeroPoints": true` to force execution
|
||||
- Check if you've already completed your daily tasks
|
||||
- Verify that your account has access to Microsoft Rewards
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Run Frequency**: Configure to run 1-2 times per day for best results
|
||||
- **Browser Visibility**: For troubleshooting, set `headless: false` to observe the automation
|
||||
- **Account Safety**: Avoid running too many accounts from the same IP address
|
||||
- **Delays**: The default search delays are designed to mimic human behavior; extreme changes might increase detection risk
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**USE AT YOUR OWN RISK**: This script is provided for educational purposes only. Using automation scripts violates Microsoft Rewards' terms of service and may result in account suspension or banning. The developers are not responsible for any consequences of using this script.
|
||||
|
||||
---
|
||||
|
||||
This project is not affiliated with Microsoft or Microsoft Rewards.
|
||||
0
README_EN.md
Normal file
0
README_EN.md
Normal file
15
compose.yaml
Normal file
15
compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
microsoft-rewards-script:
|
||||
build: .
|
||||
container_name: microsoft-rewards-script
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
### Replace "/path/to/" with the actual path to where you want to save the files on your local machine.
|
||||
- /path/to/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json
|
||||
- /path/to/config.json:/usr/src/microsoft-rewards-script/dist/config.json
|
||||
- /path/to/sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- CRON_SCHEDULE=0 7,15,20 * * * # Customize your schedule, use crontab.guru for formatting
|
||||
- RUN_ON_START=true # Runs the script on container startup
|
||||
- TZ=America/Toronto # Set your timezone for proper scheduling
|
||||
51
package.json
Normal file
51
package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "1.5.2",
|
||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-build": "npm i && rimraf dist && npx playwright install chromium",
|
||||
"build": "tsc",
|
||||
"start": "node ./dist/index.js",
|
||||
"ts-start": "ts-node ./src/index.ts",
|
||||
"dev": "ts-node ./src/index.ts -dev",
|
||||
"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 ."
|
||||
},
|
||||
"keywords": [
|
||||
"Bing Rewards",
|
||||
"Microsoft Rewards",
|
||||
"Bot",
|
||||
"Script",
|
||||
"TypeScript",
|
||||
"Playwright",
|
||||
"Cheerio"
|
||||
],
|
||||
"author": "Netsky",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ms": "^0.7.34",
|
||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-modules-newline": "^0.0.6",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"fingerprint-generator": "^2.1.66",
|
||||
"fingerprint-injector": "^2.1.66",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ms": "^2.1.3",
|
||||
"playwright": "1.47.2",
|
||||
"rebrowser-playwright": "1.47.2",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
24
src/accounts.example.json
Normal file
24
src/accounts.example.json
Normal file
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"email": "email_1",
|
||||
"password": "password_1",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"email": "email_2",
|
||||
"password": "password_2",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
138
src/browser/Browser.ts
Normal file
138
src/browser/Browser.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import playwright, { BrowserContext } from 'rebrowser-playwright'
|
||||
import { newInjectedContext } from 'fingerprint-injector'
|
||||
import { FingerprintGenerator } from 'fingerprint-generator'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||
import { updateFingerprintUserAgent } from '../util/UserAgent'
|
||||
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
|
||||
/* Test Stuff
|
||||
https://abrahamjuliot.github.io/creepjs/
|
||||
https://botcheck.luminati.io/
|
||||
https://fv.pro/
|
||||
https://pixelscan.net/
|
||||
https://www.browserscan.net/
|
||||
*/
|
||||
|
||||
class Browser {
|
||||
private bot: MicrosoftRewardsBot
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
|
||||
try {
|
||||
// Create a unique temporary folder for each browser session
|
||||
// to avoid reusing old data
|
||||
const tempUserDataDir = path.resolve(`./temp-browser-profile-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`);
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Creating new browser session with clean profile: ${tempUserDataDir}`);
|
||||
|
||||
// Ensure the directory exists
|
||||
await fs.mkdir(tempUserDataDir, { recursive: true }).catch(err => {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Error creating directory: ${err.message}`, 'warn');
|
||||
});
|
||||
|
||||
// Browser launch options
|
||||
const launchOptions = {
|
||||
headless: this.bot.config.headless,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--mute-audio',
|
||||
'--disable-setuid-sandbox',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors',
|
||||
'--disable-features=IsolateOrigins,site-per-process',
|
||||
'--disable-site-isolation-trials',
|
||||
'--disable-web-security',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--password-store=basic',
|
||||
'--disable-restore-session-state',
|
||||
...(this.bot.isMobile ? ['--window-size=375,667', '--window-position=0,0'] : [])
|
||||
]
|
||||
};
|
||||
|
||||
// Proxy configuration if provided
|
||||
const proxyOptions = proxy.url ? {
|
||||
proxy: {
|
||||
server: `${proxy.url}:${proxy.port}`,
|
||||
...(proxy.username && { username: proxy.username }),
|
||||
...(proxy.password && { password: proxy.password })
|
||||
}
|
||||
} : {};
|
||||
|
||||
// Viewport dimensions based on mobile or desktop mode
|
||||
const viewportDimensions = this.bot.isMobile
|
||||
? { width: 320, height: 568 } // iPhone 5/SE dimensions
|
||||
: { width: 1280, height: 720 };
|
||||
|
||||
// Context options
|
||||
const contextOptions = {
|
||||
...proxyOptions,
|
||||
viewport: viewportDimensions,
|
||||
isMobile: this.bot.isMobile,
|
||||
hasTouch: this.bot.isMobile,
|
||||
deviceScaleFactor: this.bot.isMobile ? 2 : 1,
|
||||
locale: 'en-US',
|
||||
timezoneId: 'America/New_York',
|
||||
permissions: ['geolocation', 'notifications']
|
||||
};
|
||||
|
||||
// Launch browser with a persistent context
|
||||
const browser = await playwright.chromium.launch(launchOptions);
|
||||
|
||||
// Load session data but don't use cookies to force a new login
|
||||
await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint);
|
||||
|
||||
// Always generate a new fingerprint at each startup to avoid detection issues
|
||||
const fingerprint = await this.generateFingerprint();
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Setting viewport to ${viewportDimensions.width}x${viewportDimensions.height} (${this.bot.isMobile ? 'mobile' : 'desktop'} mode)`);
|
||||
|
||||
// Create a context with the injected fingerprint and user data directory
|
||||
const context = await newInjectedContext(browser as any, {
|
||||
fingerprint: fingerprint,
|
||||
newContextOptions: {
|
||||
...contextOptions,
|
||||
userDataDir: tempUserDataDir
|
||||
}
|
||||
});
|
||||
|
||||
// Set default timeout
|
||||
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000));
|
||||
|
||||
// Save the new fingerprint for future use
|
||||
if (this.bot.config.saveFingerprint) {
|
||||
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint);
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`);
|
||||
|
||||
return context as BrowserContext;
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Error creating browser: ${error}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async generateFingerprint() {
|
||||
const fingerPrintData = new FingerprintGenerator().getFingerprint({
|
||||
devices: this.bot.isMobile ? ['mobile'] : ['desktop'],
|
||||
operatingSystems: this.bot.isMobile ? ['android'] : ['windows'],
|
||||
browsers: [{ name: 'edge' }]
|
||||
});
|
||||
|
||||
const updatedFingerPrintData = await updateFingerprintUserAgent(fingerPrintData, this.bot.isMobile);
|
||||
|
||||
return updatedFingerPrintData;
|
||||
}
|
||||
}
|
||||
|
||||
export default Browser
|
||||
387
src/browser/BrowserFunc.ts
Normal file
387
src/browser/BrowserFunc.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
import { BrowserContext, Page } from 'rebrowser-playwright'
|
||||
import { CheerioAPI, load } from 'cheerio'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import { promises as fs } from 'fs'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
|
||||
import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData'
|
||||
import { QuizData } from './../interface/QuizData'
|
||||
import { AppUserData } from '../interface/AppUserData'
|
||||
import { EarnablePoints } from '../interface/Points'
|
||||
|
||||
|
||||
export default class BrowserFunc {
|
||||
private bot: MicrosoftRewardsBot
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate the provided page to rewards homepage
|
||||
* @param {Page} page Playwright page
|
||||
*/
|
||||
async goHome(page: Page) {
|
||||
|
||||
try {
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
|
||||
if (page.url() === dashboardURL.href) {
|
||||
return
|
||||
}
|
||||
|
||||
await page.goto(this.bot.config.baseURL)
|
||||
|
||||
const maxIterations = 5 // Maximum iterations set to 5
|
||||
|
||||
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||
await this.bot.utils.wait(3000)
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
// Check if account is suspended
|
||||
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (isSuspended) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account is suspended!', 'error')
|
||||
throw new Error('Account has been suspended!')
|
||||
}
|
||||
|
||||
try {
|
||||
// If activities are found, exit the loop
|
||||
await page.waitForSelector('#more-activities', { timeout: 1000 })
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
|
||||
break
|
||||
|
||||
} catch (error) {
|
||||
// Continue if element is not found
|
||||
}
|
||||
|
||||
// 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(2000)
|
||||
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(5000)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user dashboard data
|
||||
* @returns {DashboardData} Object of user bing rewards dashboard data
|
||||
*/
|
||||
async getDashboardData(): Promise<DashboardData> {
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
const currentURL = new URL(this.bot.homePage.url())
|
||||
|
||||
try {
|
||||
// Should never happen since tasks are opened in a new tab!
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||
await this.goHome(this.bot.homePage)
|
||||
}
|
||||
|
||||
// Reload the page to get new data
|
||||
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
|
||||
|
||||
const scriptContent = await this.bot.homePage.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
|
||||
return targetScript?.innerText ? targetScript.innerText : null
|
||||
})
|
||||
|
||||
if (!scriptContent) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
||||
}
|
||||
|
||||
// Extract the dashboard object from the script content
|
||||
const dashboardData = await this.bot.homePage.evaluate((scriptContent: string) => {
|
||||
// Extract the dashboard object using regex
|
||||
const regex = /var dashboard = (\{.*?\});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
|
||||
if (match && match[1]) {
|
||||
return JSON.parse(match[1])
|
||||
}
|
||||
|
||||
}, scriptContent)
|
||||
|
||||
if (!dashboardData) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
|
||||
}
|
||||
|
||||
return dashboardData
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Error fetching dashboard data: ${error}`, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search point counters
|
||||
* @returns {Counters} Object of search counter data
|
||||
*/
|
||||
async getSearchPoints(): Promise<Counters> {
|
||||
const dashboardData = await this.getDashboardData() // Always fetch newest data
|
||||
|
||||
return dashboardData.userStatus.counters
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total earnable points with web browser
|
||||
* @returns {number} Total earnable points
|
||||
*/
|
||||
async getBrowserEarnablePoints(): Promise<EarnablePoints> {
|
||||
try {
|
||||
let desktopSearchPoints = 0
|
||||
let mobileSearchPoints = 0
|
||||
let dailySetPoints = 0
|
||||
let morePromotionsPoints = 0
|
||||
|
||||
const data = await this.getDashboardData()
|
||||
|
||||
// Desktop Search Points
|
||||
if (data.userStatus.counters.pcSearch?.length) {
|
||||
data.userStatus.counters.pcSearch.forEach(x => desktopSearchPoints += (x.pointProgressMax - x.pointProgress))
|
||||
}
|
||||
|
||||
// Mobile Search Points
|
||||
if (data.userStatus.counters.mobileSearch?.length) {
|
||||
data.userStatus.counters.mobileSearch.forEach(x => mobileSearchPoints += (x.pointProgressMax - x.pointProgress))
|
||||
}
|
||||
|
||||
// Daily Set
|
||||
data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => dailySetPoints += (x.pointProgressMax - x.pointProgress))
|
||||
|
||||
// More Promotions
|
||||
if (data.morePromotions?.length) {
|
||||
data.morePromotions.forEach((x: MorePromotion) => {
|
||||
// Only count points from supported activities
|
||||
if (['quiz', 'urlreward'].includes(x.promotionType) && x.exclusiveLockedFeatureStatus !== 'locked') {
|
||||
morePromotionsPoints += (x.pointProgressMax - x.pointProgress)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const totalEarnablePoints = desktopSearchPoints + mobileSearchPoints + dailySetPoints + morePromotionsPoints
|
||||
|
||||
return {
|
||||
dailySetPoints,
|
||||
morePromotionsPoints,
|
||||
desktopSearchPoints,
|
||||
mobileSearchPoints,
|
||||
totalEarnablePoints
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total earnable points with mobile app
|
||||
* @returns {number} Total earnable points
|
||||
*/
|
||||
async getAppEarnablePoints(accessToken: string) {
|
||||
try {
|
||||
const points = {
|
||||
readToEarn: 0,
|
||||
checkIn: 0,
|
||||
totalEarnablePoints: 0
|
||||
}
|
||||
|
||||
const eligibleOffers = [
|
||||
'ENUS_readarticle3_30points',
|
||||
'Gamification_Sapphire_DailyCheckIn'
|
||||
]
|
||||
|
||||
const data = await this.getDashboardData()
|
||||
let geoLocale = data.userProfile.attributes.country
|
||||
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||
|
||||
const userDataRequest: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
}
|
||||
}
|
||||
|
||||
const userDataResponse: AppUserData = (await this.bot.axios.request(userDataRequest)).data
|
||||
const userData = userDataResponse.response
|
||||
const eligibleActivities = userData.promotions.filter((x) => eligibleOffers.includes(x.attributes.offerid ?? ''))
|
||||
|
||||
for (const item of eligibleActivities) {
|
||||
if (item.attributes.type === 'msnreadearn') {
|
||||
points.readToEarn = parseInt(item.attributes.pointmax ?? '') - parseInt(item.attributes.pointprogress ?? '')
|
||||
break
|
||||
} else if (item.attributes.type === 'checkin') {
|
||||
const checkInDay = parseInt(item.attributes.progress ?? '') % 7
|
||||
|
||||
if (checkInDay < 6 && (new Date()).getDate() != (new Date(item.attributes.last_updated ?? '')).getDate()) {
|
||||
points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
points.totalEarnablePoints = points.readToEarn + points.checkIn
|
||||
|
||||
return points
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current point amount
|
||||
* @returns {number} Current total point amount
|
||||
*/
|
||||
async getCurrentPoints(): Promise<number> {
|
||||
try {
|
||||
const data = await this.getDashboardData()
|
||||
|
||||
return data.userStatus.availablePoints
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse quiz data from provided page
|
||||
* @param {Page} page Playwright page
|
||||
* @returns {QuizData} Quiz data object
|
||||
*/
|
||||
async getQuizData(page: Page): Promise<QuizData> {
|
||||
try {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const scriptContent = $('script').filter((index, element) => {
|
||||
return $(element).text().includes('_w.rewardsQuizRenderInfo')
|
||||
}).text()
|
||||
|
||||
if (scriptContent) {
|
||||
const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
|
||||
if (match && match[1]) {
|
||||
const quizData = JSON.parse(match[1])
|
||||
return quizData
|
||||
} else {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Quiz data not found within script', 'error')
|
||||
}
|
||||
} else {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async waitForQuizRefresh(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10000 })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'An error occurred:' + error, 'error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async checkQuizCompleted(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async loadInCheerio(page: Page): Promise<CheerioAPI> {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
return $
|
||||
}
|
||||
|
||||
async getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise<string> {
|
||||
let selector = ''
|
||||
try {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const element = $('.offer-cta').toArray().find(x => x.attribs.href?.includes(activity.offerId))
|
||||
if (element) {
|
||||
selector = `a[href*="${element.attribs.href}"]`
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
async closeBrowser(browser: BrowserContext, email: string) {
|
||||
try {
|
||||
// Save cookies if configured
|
||||
if (this.bot.config.sessionPath) {
|
||||
await saveSessionData(this.bot.config.sessionPath, browser, email, this.bot.isMobile);
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Session data saved successfully');
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000);
|
||||
|
||||
// Utiliser la méthode centralisée pour nettoyer la session du navigateur
|
||||
await this.bot.browser.utils.cleanBrowserSession(browser);
|
||||
|
||||
// Get folder name from userDataDir to clean up
|
||||
const browserContextData = browser as any;
|
||||
const userDataDir = browserContextData._options?.userDataDir;
|
||||
|
||||
// Close browser
|
||||
await browser.close();
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!');
|
||||
|
||||
// Clean up the temporary profile directory if it exists
|
||||
if (userDataDir) {
|
||||
const fs = require('fs').promises;
|
||||
try {
|
||||
await fs.rm(userDataDir, { recursive: true, force: true });
|
||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `Removed temporary profile: ${userDataDir}`);
|
||||
} catch (cleanupError) {
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'CLOSE-BROWSER',
|
||||
`Failed to remove temporary profile: ${cleanupError}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred:' + error, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
238
src/browser/BrowserUtil.ts
Normal file
238
src/browser/BrowserUtil.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { BrowserContext, Page } from 'rebrowser-playwright'
|
||||
import { load } from 'cheerio'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
|
||||
export default class BrowserUtil {
|
||||
private bot: MicrosoftRewardsBot
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async tryDismissAllMessages(page: Page): Promise<boolean> {
|
||||
const buttons = [
|
||||
{ selector: 'button[type="submit"]', label: 'Submit Button' },
|
||||
{ selector: '#acceptButton', label: 'AcceptButton' },
|
||||
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
|
||||
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
|
||||
{ selector: '#iShowSkip', label: 'iShowSkip' },
|
||||
{ selector: '#iNext', label: 'iNext' },
|
||||
{ selector: '#iLooksGood', label: 'iLooksGood' },
|
||||
{ selector: '#idSIButton9', label: 'idSIButton9' },
|
||||
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' },
|
||||
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' },
|
||||
{ selector: '.maybe-later', label: 'Mobile Rewards App Banner' },
|
||||
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container', isXPath: true },
|
||||
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
|
||||
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
|
||||
]
|
||||
|
||||
const dismissTasks = buttons.map(async (button) => {
|
||||
try {
|
||||
const element = button.isXPath
|
||||
? page.locator(`xpath=${button.selector}`)
|
||||
: page.locator(button.selector)
|
||||
|
||||
if (await element.first().isVisible({ timeout: 1000 })) {
|
||||
await element.first().click({ timeout: 1000 })
|
||||
await page.waitForTimeout(500)
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Silent fail
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const results = await Promise.allSettled(dismissTasks)
|
||||
return results.some(result => result.status === 'fulfilled' && result.value === true)
|
||||
}
|
||||
|
||||
async getLatestTab(page: Page): Promise<Page> {
|
||||
try {
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
const browser = page.context()
|
||||
const pages = browser.pages()
|
||||
const newTab = pages[pages.length - 1]
|
||||
|
||||
if (newTab) {
|
||||
return newTab
|
||||
}
|
||||
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Unable to get latest tab', 'error')
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async getTabs(page: Page) {
|
||||
try {
|
||||
const browser = page.context()
|
||||
const pages = browser.pages()
|
||||
|
||||
const homeTab = pages[1]
|
||||
let homeTabURL: URL
|
||||
|
||||
if (!homeTab) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Home tab could not be found!', 'error')
|
||||
|
||||
} else {
|
||||
homeTabURL = new URL(homeTab.url())
|
||||
|
||||
if (homeTabURL.hostname !== 'rewards.bing.com') {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const workerTab = pages[2]
|
||||
if (!workerTab) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Worker tab could not be found!', 'error')
|
||||
}
|
||||
|
||||
return {
|
||||
homeTab: homeTab,
|
||||
workerTab: workerTab
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async reloadBadPage(page: Page): Promise<void> {
|
||||
try {
|
||||
const html = await page.content().catch(() => '')
|
||||
const $ = load(html)
|
||||
|
||||
const isNetworkError = $('body.neterror').length
|
||||
|
||||
if (isNetworkError) {
|
||||
this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', 'Bad page detected, reloading!')
|
||||
await page.reload()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans browser cookies and storage data
|
||||
* This function centralizes the cleaning logic to ensure a fresh session
|
||||
* @param {BrowserContext} browser - The browser context to clean
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async cleanBrowserSession(browser: BrowserContext): Promise<void> {
|
||||
try {
|
||||
// Vérifier si le navigateur est toujours utilisable
|
||||
if (!browser || !browser.pages().length) {
|
||||
this.bot.log(this.bot.isMobile, 'CLEAN-BROWSER', 'Browser context already closed or invalid', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean all browser cookies
|
||||
await browser.clearCookies().catch(e => {
|
||||
this.bot.log(this.bot.isMobile, 'CLEAN-BROWSER', `Error clearing cookies: ${e}`, 'warn');
|
||||
});
|
||||
this.bot.log(this.bot.isMobile, 'CLEAN-BROWSER', 'Cleared browser cookies');
|
||||
|
||||
// Clean localStorage and sessionStorage for all pages
|
||||
const cleanupTasks = browser.pages().map(async (page) => {
|
||||
try {
|
||||
// Vérifier si la page est encore ouverte
|
||||
if (page.isClosed()) return;
|
||||
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Supprimer tous les cookies du document
|
||||
const cookies = document.cookie.split(';');
|
||||
for (const cookie of cookies) {
|
||||
const [name] = cookie.trim().split('=');
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;`;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs de même origine
|
||||
return false;
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignorer les erreurs si la page a été fermée pendant l'exécution
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs si le stockage ne peut pas être effacé
|
||||
}
|
||||
});
|
||||
|
||||
// Attendre que toutes les tâches de nettoyage soient terminées
|
||||
await Promise.allSettled(cleanupTasks);
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'CLEAN-BROWSER', 'Cleared browser storage data');
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'CLEAN-BROWSER', `Error cleaning browser session: ${error}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page corresponds to a specific Microsoft screen
|
||||
* This function quickly detects which login page we're currently on
|
||||
* @param {Page} page - The page to analyze
|
||||
* @returns {Promise<string|null>} - The detected screen type or null
|
||||
*/
|
||||
async detectMicrosoftScreen(page: Page): Promise<string|null> {
|
||||
try {
|
||||
// Check for recovery email screen
|
||||
const recoveryEmailInput = await page.waitForSelector(
|
||||
'input[type="email"][aria-label*="recovery"], input[name="RecoveryEmailAddress"], #RecoveryEmailAddress, input[placeholder*="recovery"], input[type="email"][id*="recovery"]',
|
||||
{ timeout: 2000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (recoveryEmailInput) return 'recovery-email';
|
||||
|
||||
// Check for account protection screen
|
||||
const protectionHeading = await page.waitForSelector(
|
||||
'h1:has-text("Let\'s protect your account"), h1:has-text("Protect your account"), div[role="heading"]:has-text("Let\'s protect your account")',
|
||||
{ timeout: 2000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (protectionHeading) return 'account-protection';
|
||||
|
||||
// Check for password screen
|
||||
const passwordInput = await page.waitForSelector(
|
||||
'input[type="password"], input[name="passwd"], input[id="i0118"]',
|
||||
{ timeout: 2000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (passwordInput) return 'password';
|
||||
|
||||
// Check for two-factor authentication screen
|
||||
const twoFactorElement = await page.waitForSelector(
|
||||
'#displaySign, #idRichContext_DisplaySign',
|
||||
{ timeout: 2000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (twoFactorElement) return '2fa';
|
||||
|
||||
// Check for "Stay signed in" screen
|
||||
const staySignedInPrompt = await page.waitForSelector(
|
||||
'#KmsiCheckboxField, input[name="DontShowAgain"], [role="heading"]:has-text("Stay signed in?")',
|
||||
{ timeout: 2000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (staySignedInPrompt) return 'stay-signed-in';
|
||||
|
||||
// No specific screen detected
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'DETECT-SCREEN', `Error detecting Microsoft screen: ${error}`, 'warn');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/config.json
Normal file
48
src/config.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
"sessionPath": "sessions",
|
||||
"headless": false,
|
||||
"parallel": true,
|
||||
"runOnZeroPoints": false,
|
||||
"clusters": 1,
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
},
|
||||
"recoveryEmail": "",
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doDesktopSearch": true,
|
||||
"doMobileSearch": true,
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true
|
||||
},
|
||||
"searchOnBingLocalQueries": false,
|
||||
"globalTimeout": "30s",
|
||||
"searchSettings": {
|
||||
"useGeoLocaleQueries": false,
|
||||
"scrollRandomResults": true,
|
||||
"clickRandomResults": true,
|
||||
"searchDelay": {
|
||||
"min": "3min",
|
||||
"max": "5min"
|
||||
},
|
||||
"retryMobileSearchAmount": 2
|
||||
},
|
||||
"logExcludeFunc": [
|
||||
"SEARCH-CLOSE-TABS"
|
||||
],
|
||||
"webhookLogExcludeFunc": [
|
||||
"SEARCH-CLOSE-TABS"
|
||||
],
|
||||
"proxy": {
|
||||
"proxyGoogleTrends": true,
|
||||
"proxyBingTerms": true
|
||||
},
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
1
src/crontab.template
Normal file
1
src/crontab.template
Normal file
@@ -0,0 +1 @@
|
||||
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /proc/1/fd/1 2>> /proc/1/fd/2
|
||||
66
src/functions/Activities.ts
Normal file
66
src/functions/Activities.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { DashboardData } from '../interface/DashboardData'
|
||||
|
||||
// Importation des activités
|
||||
import SearchOnBing from './activities/SearchOnBing'
|
||||
import ReadToEarn from './activities/ReadToEarn'
|
||||
import DailyCheckIn from './activities/DailyCheckIn'
|
||||
// Importer d'autres activités au besoin
|
||||
|
||||
export default class Activities {
|
||||
private bot: MicrosoftRewardsBot
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
|
||||
const search = new Search(this.bot)
|
||||
await search.doSearch(page, data)
|
||||
}
|
||||
|
||||
doABC = async (page: Page): Promise<void> => {
|
||||
const abc = new ABC(this.bot)
|
||||
await abc.doABC(page)
|
||||
}
|
||||
|
||||
doPoll = async (page: Page): Promise<void> => {
|
||||
const poll = new Poll(this.bot)
|
||||
await poll.doPoll(page)
|
||||
}
|
||||
|
||||
doThisOrThat = async (page: Page): Promise<void> => {
|
||||
const thisOrThat = new ThisOrThat(this.bot)
|
||||
await thisOrThat.doThisOrThat(page)
|
||||
}
|
||||
|
||||
doQuiz = async (page: Page): Promise<void> => {
|
||||
const quiz = new Quiz(this.bot)
|
||||
await quiz.doQuiz(page)
|
||||
}
|
||||
|
||||
doUrlReward = async (page: Page): Promise<void> => {
|
||||
const urlReward = new UrlReward(this.bot)
|
||||
await urlReward.doUrlReward(page)
|
||||
}
|
||||
|
||||
doSearchOnBing = async (page: Page, activity: MorePromotion | PromotionalItem): Promise<void> => {
|
||||
const searchOnBing = new SearchOnBing(this.bot)
|
||||
await searchOnBing.doSearchOnBing(page, activity)
|
||||
}
|
||||
|
||||
doReadToEarn = async (accessToken: string, data: DashboardData): Promise<void> => {
|
||||
const readToEarn = new ReadToEarn(this.bot)
|
||||
await readToEarn.doReadToEarn(accessToken, data)
|
||||
}
|
||||
|
||||
doDailyCheckIn = async (accessToken: string, data: DashboardData): Promise<void> => {
|
||||
const dailyCheckIn = new DailyCheckIn(this.bot)
|
||||
await dailyCheckIn.doDailyCheckIn(accessToken, data)
|
||||
}
|
||||
|
||||
}
|
||||
791
src/functions/Login.ts
Normal file
791
src/functions/Login.ts
Normal file
@@ -0,0 +1,791 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import readline from 'readline'
|
||||
import * as crypto from 'crypto'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
|
||||
import { OAuth } from '../interface/OAuth'
|
||||
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
export class Login {
|
||||
private bot: MicrosoftRewardsBot
|
||||
private clientId: string = '0000000040170455'
|
||||
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
|
||||
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
|
||||
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
|
||||
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
||||
// New login URL for Microsoft's updated login system
|
||||
private microsoftLoginUrl: string = 'https://login.microsoftonline.com/'
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async login(page: Page, email: string, password: string) {
|
||||
try {
|
||||
// Navigate to the new Microsoft login page instead of Bing login
|
||||
await page.goto(this.microsoftLoginUrl)
|
||||
|
||||
await page.waitForLoadState('domcontentloaded').catch(() => { })
|
||||
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
|
||||
// Check if account is locked
|
||||
await this.checkAccountLocked(page)
|
||||
|
||||
// Vérifier si nous sommes déjà à une étape spécifique du processus de connexion
|
||||
// et agir en conséquence (utile si le navigateur est réouvert à mi-parcours)
|
||||
await this.checkCurrentLoginState(page, email, password)
|
||||
|
||||
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// Utiliser la méthode de détection d'écran Microsoft pour identifier l'état actuel
|
||||
const currentScreen = await this.bot.browser.utils.detectMicrosoftScreen(page);
|
||||
|
||||
// Si un écran a été détecté, traiter en fonction du type d'écran
|
||||
if (currentScreen) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Detected Microsoft screen: ${currentScreen}`, 'log', 'yellow');
|
||||
|
||||
switch (currentScreen) {
|
||||
case 'password':
|
||||
await this.enterPassword(page, password);
|
||||
break;
|
||||
case 'recovery-email':
|
||||
await this.handleRecoveryEmail(page);
|
||||
break;
|
||||
case 'account-protection':
|
||||
await this.handleAccountProtection(page);
|
||||
break;
|
||||
case '2fa':
|
||||
await this.handle2FA(page);
|
||||
break;
|
||||
case 'stay-signed-in':
|
||||
await this.handleStaySignedIn(page);
|
||||
break;
|
||||
default:
|
||||
// Si l'écran est inconnu, commencer la séquence normale
|
||||
await this.execLogin(page, email, password);
|
||||
}
|
||||
} else {
|
||||
// Si aucun écran spécifique n'est détecté, commencer la séquence de connexion normale
|
||||
await this.execLogin(page, email, password);
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft successfully')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in')
|
||||
|
||||
// Check if account is locked
|
||||
await this.checkAccountLocked(page)
|
||||
}
|
||||
|
||||
// Navigate to rewards after login
|
||||
await page.goto('https://rewards.bing.com')
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
// Check if logged in to bing
|
||||
await this.checkBingLogin(page)
|
||||
|
||||
// Save session
|
||||
await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile)
|
||||
|
||||
// We're done logging in
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged in successfully, saved login session!')
|
||||
|
||||
} catch (error) {
|
||||
// Throw and don't continue
|
||||
throw this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async execLogin(page: Page, email: string, password: string) {
|
||||
try {
|
||||
await this.enterEmail(page, email)
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.enterPassword(page, password)
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
// Handle account protection prompt if present
|
||||
await this.handleAccountProtection(page)
|
||||
|
||||
// Handle recovery email prompt if present
|
||||
await this.handleRecoveryEmail(page)
|
||||
|
||||
// Handle "Stay signed in?" prompt
|
||||
await this.handleStaySignedIn(page)
|
||||
|
||||
// Check if account is locked
|
||||
await this.checkAccountLocked(page)
|
||||
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
await this.checkLoggedIn(page)
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred: ' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async enterEmail(page: Page, email: string) {
|
||||
// Updated selectors for the new Microsoft login page
|
||||
const emailInputSelector = 'input[type="email"], input[name="loginfmt"]'
|
||||
|
||||
try {
|
||||
// Wait for email field
|
||||
const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
|
||||
if (!emailField) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
|
||||
return
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
// Check if email is prefilled
|
||||
const emailPrefilled = await page.waitForSelector('#displayName, #userDisplayName', { timeout: 5000 }).catch(() => null)
|
||||
if (emailPrefilled) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
|
||||
} else {
|
||||
// Else clear and fill email
|
||||
await page.fill(emailInputSelector, '')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.fill(emailInputSelector, email)
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
// Use role selector for the "Next" button which is more resilient to changes
|
||||
const nextButton = await page.waitForSelector('button[type="submit"], input[type="submit"], [role="button"]:has-text("Next")', { timeout: 5000 }).catch(() => null)
|
||||
if (nextButton) {
|
||||
await nextButton.click()
|
||||
await this.bot.utils.wait(2000)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async enterPassword(page: Page, password: string) {
|
||||
// Updated selectors for the new Microsoft login page
|
||||
const passwordInputSelector = 'input[type="password"], input[name="passwd"], input[id="i0118"]'
|
||||
|
||||
try {
|
||||
// Wait for password field
|
||||
const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
|
||||
if (!passwordField) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
|
||||
await this.handle2FA(page)
|
||||
return
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
// Clear and fill password
|
||||
await page.fill(passwordInputSelector, '')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.fill(passwordInputSelector, password)
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
// Use more resilient selectors for the sign-in button
|
||||
const signInButton = await page.waitForSelector('button[type="submit"], input[type="submit"], #idSIButton9', { timeout: 5000 }).catch(() => null)
|
||||
if (signInButton) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicking sign-in button after password entry')
|
||||
await signInButton.click()
|
||||
|
||||
// Wait for page to load after password submission
|
||||
await this.bot.utils.wait(3000)
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully, checking for follow-up screens...')
|
||||
|
||||
// Immediately check for recovery email or account protection screens
|
||||
// This is a critical step - Microsoft often shows these screens right after password entry
|
||||
|
||||
// First check for account protection screen which is often shown immediately
|
||||
const protectionHeading = await page.waitForSelector(
|
||||
'h1:has-text("Let\'s protect your account"), h1:has-text("Protect your account"), div[role="heading"]:has-text("Let\'s protect your account")',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (protectionHeading) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Account protection screen detected right after password entry', 'log', 'yellow')
|
||||
await this.handleAccountProtection(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Then check for recovery email prompt
|
||||
const recoveryEmailInput = await page.waitForSelector(
|
||||
'input[type="email"][aria-label*="recovery"], input[name="RecoveryEmailAddress"], #RecoveryEmailAddress, input[placeholder*="recovery"], input[type="email"][id*="recovery"]',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (recoveryEmailInput) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery email prompt detected right after password entry', 'log', 'yellow')
|
||||
await this.handleRecoveryEmail(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for "Stay signed in?" prompt
|
||||
const staySignedInPrompt = await page.waitForSelector(
|
||||
'#KmsiCheckboxField, input[name="DontShowAgain"], [role="heading"]:has-text("Stay signed in?")',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (staySignedInPrompt) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '"Stay signed in" prompt detected after password entry', 'log', 'yellow')
|
||||
await this.handleStaySignedIn(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for the possibility of 2FA being required
|
||||
const twoFactorElement = await page.waitForSelector(
|
||||
'#displaySign, #idRichContext_DisplaySign',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (twoFactorElement) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA verification detected after password entry', 'log', 'yellow')
|
||||
await this.handle2FA(page)
|
||||
return
|
||||
}
|
||||
|
||||
// If we've reached here, we're probably successfully logged in
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'No follow-up screens detected after password entry')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Sign in button not found after password entry', 'warn')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry flow error: ${error}`, 'error')
|
||||
// Check if we need to handle 2FA
|
||||
const twoFactorDetected = await page.waitForSelector('#displaySign, #idRichContext_DisplaySign', { timeout: 2000 }).catch(() => null)
|
||||
if (twoFactorDetected) {
|
||||
await this.handle2FA(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStaySignedIn(page: Page) {
|
||||
try {
|
||||
// Wait for the "Stay signed in?" prompt
|
||||
const staySignedInPrompt = await page.waitForSelector('#KmsiCheckboxField, input[name="DontShowAgain"], [role="heading"]:has-text("Stay signed in?")', { timeout: 5000 }).catch(() => null)
|
||||
|
||||
if (staySignedInPrompt) {
|
||||
// Check the "Stay signed in" checkbox if it exists
|
||||
await page.waitForSelector('#KmsiCheckboxField, input[name="DontShowAgain"]', { timeout: 2000 })
|
||||
.then(async (checkbox: any) => {
|
||||
if (checkbox) {
|
||||
await checkbox.check().catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Click "Yes" button
|
||||
const yesButton = await page.waitForSelector('input[type="submit"][value="Yes"], button:has-text("Yes"), #idSIButton9', { timeout: 2000 }).catch(() => null)
|
||||
if (yesButton) {
|
||||
await yesButton.click();
|
||||
await this.bot.utils.wait(2000);
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Yes" for stay signed in');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `No "Stay signed in?" prompt found: ${error}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
private async handle2FA(page: Page) {
|
||||
try {
|
||||
const numberToPress = await this.get2FACode(page)
|
||||
if (numberToPress) {
|
||||
// Authentictor App verification
|
||||
await this.authAppVerification(page, numberToPress)
|
||||
} else {
|
||||
// SMS verification
|
||||
await this.authSMSVerification(page)
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `2FA handling failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async get2FACode(page: Page): Promise<string | null> {
|
||||
try {
|
||||
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
|
||||
return await element.textContent()
|
||||
} catch {
|
||||
if (this.bot.config.parallel) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Script running in parallel, can only send 1 2FA request per account at a time!', 'log', 'yellow')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Trying again in 60 seconds! Please wait...', 'log', 'yellow')
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
|
||||
if (button) {
|
||||
await this.bot.utils.wait(60000)
|
||||
await button.click()
|
||||
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
|
||||
await this.bot.utils.wait(2000)
|
||||
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
|
||||
return await element.textContent()
|
||||
}
|
||||
}
|
||||
|
||||
private async authAppVerification(page: Page, numberToPress: string | null) {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
|
||||
|
||||
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
|
||||
break
|
||||
} catch {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'The code is expired. Trying to get a new code...')
|
||||
await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]')
|
||||
numberToPress = await this.get2FACode(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async authSMSVerification(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'SMS 2FA code required. Waiting for user input...')
|
||||
|
||||
const code = await new Promise<string>((resolve) => {
|
||||
rl.question('Enter 2FA code:\n', (input: string) => {
|
||||
rl.close()
|
||||
resolve(input)
|
||||
})
|
||||
})
|
||||
|
||||
await page.fill('input[name="otc"]', code)
|
||||
await page.keyboard.press('Enter')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code entered successfully')
|
||||
}
|
||||
|
||||
private async checkLoggedIn(page: Page) {
|
||||
// After login, we need to navigate to rewards.bing.com
|
||||
try {
|
||||
await page.goto('https://rewards.bing.com', { waitUntil: 'networkidle' });
|
||||
|
||||
// Wait for the rewards portal to load
|
||||
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 15000 })
|
||||
.then(() => {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal');
|
||||
})
|
||||
.catch(() => {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not verify if logged into the rewards portal', 'warn');
|
||||
});
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Error navigating to rewards portal: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
private async checkBingLogin(page: Page): Promise<void> {
|
||||
try {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing login')
|
||||
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')
|
||||
|
||||
const maxIterations = 5
|
||||
|
||||
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||
const currentUrl = new URL(page.url())
|
||||
|
||||
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
const loggedIn = await this.checkBingLoginStatus(page)
|
||||
// If mobile browser, skip this step
|
||||
if (loggedIn || this.bot.isMobile) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing login verification passed!')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async checkBingLoginStatus(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('#id_n', { timeout: 5000 })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getMobileAccessToken(page: Page, email: string) {
|
||||
const authorizeUrl = new URL(this.authBaseUrl)
|
||||
|
||||
authorizeUrl.searchParams.append('response_type', 'code')
|
||||
authorizeUrl.searchParams.append('client_id', this.clientId)
|
||||
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
|
||||
authorizeUrl.searchParams.append('scope', this.scope)
|
||||
authorizeUrl.searchParams.append('state', crypto.randomBytes(16).toString('hex'))
|
||||
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||
authorizeUrl.searchParams.append('login_hint', email)
|
||||
|
||||
await page.goto(authorizeUrl.href)
|
||||
|
||||
let currentUrl = new URL(page.url())
|
||||
let code: string
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Waiting for authorization...')
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
|
||||
code = currentUrl.searchParams.get('code')!
|
||||
break
|
||||
}
|
||||
|
||||
currentUrl = new URL(page.url())
|
||||
await this.bot.utils.wait(5000)
|
||||
}
|
||||
|
||||
const body = new URLSearchParams()
|
||||
body.append('grant_type', 'authorization_code')
|
||||
body.append('client_id', this.clientId)
|
||||
body.append('code', code)
|
||||
body.append('redirect_uri', this.redirectUrl)
|
||||
|
||||
const tokenRequest: AxiosRequestConfig = {
|
||||
url: this.tokenUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
data: body.toString()
|
||||
}
|
||||
|
||||
const tokenResponse = await this.bot.axios.request(tokenRequest)
|
||||
const tokenData: OAuth = await tokenResponse.data
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Successfully authorized')
|
||||
return tokenData.access_token
|
||||
}
|
||||
|
||||
private async checkAccountLocked(page: Page) {
|
||||
await this.bot.utils.wait(2000)
|
||||
const isLocked = await page.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
|
||||
if (isLocked) {
|
||||
throw this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'This account has been locked! Remove the account from "accounts.json" and restart!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the recovery email prompt if Microsoft requests it after password entry
|
||||
*/
|
||||
private async handleRecoveryEmail(page: Page) {
|
||||
try {
|
||||
// Log that we're checking for recovery email prompt
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Checking for Microsoft recovery email prompt...')
|
||||
|
||||
// Wait for any recovery email input to appear - common selectors that might be used
|
||||
const recoveryEmailPrompt = await page.waitForSelector(
|
||||
// Possible email input selectors
|
||||
'input[type="email"][aria-label*="recovery"], input[name="RecoveryEmailAddress"], #RecoveryEmailAddress, input[placeholder*="recovery"], input[type="email"][id*="recovery"]',
|
||||
{ timeout: 5000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (recoveryEmailPrompt) {
|
||||
// CLEARLY log that we found the recovery email screen with high visibility
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '================================================================', 'log', 'red')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '===== RECOVERY EMAIL SCREEN DETECTED =====', 'log', 'red')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '================================================================', 'log', 'red')
|
||||
|
||||
// Try to get any heading or text that describes the recovery email request
|
||||
const promptHeading = await page.evaluate(() => {
|
||||
// Look for headings or paragraphs that might contain recovery email text
|
||||
const heading = document.querySelector('h1, h2, h3, h4, p:first-of-type');
|
||||
return heading ? heading.textContent?.trim() : null;
|
||||
}).catch(() => null);
|
||||
|
||||
if (promptHeading) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Recovery prompt text: "${promptHeading}"`, 'log', 'yellow');
|
||||
}
|
||||
|
||||
// Take a screenshot to help troubleshoot
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
await page.screenshot({ path: `recovery-email-detected-${timestamp}.png`, fullPage: true });
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `→ Screenshot saved: recovery-email-detected-${timestamp}.png`, 'log', 'yellow');
|
||||
} catch (err) {
|
||||
// Ignore screenshot errors
|
||||
}
|
||||
|
||||
// Check if we have a recovery email configured
|
||||
if (this.bot.config.recoveryEmail && this.bot.config.recoveryEmail.trim() !== '') {
|
||||
// Get the actual value of the configured recovery email for logging
|
||||
const configuredEmail = this.bot.config.recoveryEmail.trim();
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `→ Using configured recovery email: ${configuredEmail.substring(0, 3)}...${configuredEmail.split('@')[1]}`, 'log', 'green');
|
||||
|
||||
// Clear the field and enter the recovery email
|
||||
await recoveryEmailPrompt.fill('');
|
||||
await this.bot.utils.wait(500);
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Entering recovery email...', 'log', 'green');
|
||||
await recoveryEmailPrompt.fill(configuredEmail);
|
||||
await this.bot.utils.wait(1000);
|
||||
|
||||
// Find and click the next/save/submit button
|
||||
const nextButton = await page.waitForSelector(
|
||||
'button[type="submit"], input[type="submit"], button:has-text("Next"), button:has-text("Save"), button:has-text("Verify")',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (nextButton) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Found submission button for recovery email', 'log', 'green');
|
||||
await nextButton.click();
|
||||
await this.bot.utils.wait(2000);
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Recovery email entered successfully', 'log', 'green');
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Could not find button to submit recovery email', 'warn');
|
||||
}
|
||||
} else {
|
||||
// If no recovery email is configured, STOP execution
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '!!!!! NO RECOVERY EMAIL CONFIGURED IN CONFIG.JSON !!!!!', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Microsoft requires a recovery email address to continue.', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Add a "recoveryEmail" field to your config.json file and restart the script.', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Example in config.json: "recoveryEmail": "your.email@example.com"', 'log', 'red');
|
||||
|
||||
// Do not try to skip, just exit the script
|
||||
throw new Error('Recovery email required but not configured in config.json. Add a recoveryEmail field and try again.');
|
||||
}
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'No recovery email prompt detected', 'log', 'green');
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Recovery email handling error: ${error}`, 'error');
|
||||
throw error; // Re-throw to stop execution
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the "Let's protect your account" screen that may appear after login
|
||||
*/
|
||||
private async handleAccountProtection(page: Page) {
|
||||
try {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Checking for account protection prompt...')
|
||||
|
||||
// Improved selectors for account protection page detection
|
||||
// Checks title AND/OR descriptive paragraphs for this page
|
||||
let protectionHeading = await page.waitForSelector(
|
||||
'h1:has-text("Let\'s protect your account"), h1:has-text("Protect your account"), div[role="heading"]:has-text("Let\'s protect your account")',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (!protectionHeading) {
|
||||
// Alternative search by text in the complete page
|
||||
const headingText = await page.evaluate(() => {
|
||||
const heading = document.querySelector('h1, h2, [role="heading"]');
|
||||
if (heading && (heading.textContent?.includes('protect your account') || heading.textContent?.includes('security info'))) {
|
||||
return heading.textContent;
|
||||
}
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
|
||||
if (headingText) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Found alternate protection heading text: "${headingText}"`, 'log', 'yellow');
|
||||
protectionHeading = { found: true } as any; // Simulate a found element
|
||||
}
|
||||
}
|
||||
|
||||
if (protectionHeading) {
|
||||
// VISUAL ALERT in the terminal that we detected the protection screen
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '================================================================', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '===== MICROSOFT ACCOUNT PROTECTION SCREEN DETECTED! =====', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '================================================================', 'log', 'red');
|
||||
|
||||
// Evaluate page content to better understand what Microsoft is asking for
|
||||
const pageContent = await page.evaluate(() => {
|
||||
return {
|
||||
text: document.body.innerText,
|
||||
html: document.body.innerHTML
|
||||
};
|
||||
}).catch(() => ({ text: '', html: '' }));
|
||||
|
||||
// Analyze content to understand available options
|
||||
const hasEmailOption = pageContent.text.toLowerCase().includes("email") ||
|
||||
pageContent.text.toLowerCase().includes("e-mail") ||
|
||||
pageContent.text.toLowerCase().includes("alternate email");
|
||||
|
||||
const hasPhoneOption = pageContent.text.toLowerCase().includes("phone") ||
|
||||
pageContent.text.toLowerCase().includes("mobile") ||
|
||||
pageContent.text.toLowerCase().includes("telephone");
|
||||
|
||||
const hasSecurity = pageContent.text.toLowerCase().includes("security");
|
||||
|
||||
// Display detailed information about what Microsoft is requesting
|
||||
if (hasEmailOption) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Microsoft is requesting a recovery email address', 'log', 'yellow');
|
||||
}
|
||||
if (hasPhoneOption) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Microsoft is requesting a phone number', 'log', 'yellow');
|
||||
}
|
||||
if (hasSecurity) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Microsoft is requesting security information', 'log', 'yellow');
|
||||
}
|
||||
|
||||
// Take a screenshot for diagnostic help
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
await page.screenshot({ path: `account-protection-detected-${timestamp}.png`, fullPage: true });
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `→ Screenshot saved: account-protection-detected-${timestamp}.png`, 'log', 'yellow');
|
||||
} catch (err) {
|
||||
// Ignore screenshot errors
|
||||
}
|
||||
|
||||
// CHECK FOR RECOVERY EMAIL CONFIGURATION
|
||||
if (!this.bot.config.recoveryEmail || this.bot.config.recoveryEmail.trim() === '') {
|
||||
// If no email address is configured, IMMEDIATE STOP
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '!!!!! NO RECOVERY EMAIL CONFIGURED IN CONFIG.JSON !!!!!', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Microsoft requires an email address to protect your account', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Add a "recoveryEmail" field in your config.json file', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Example in config.json: "recoveryEmail": "your.email@example.com"', 'log', 'red');
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Restart the script after adding this email address', 'log', 'red');
|
||||
|
||||
// Immediate script termination, no attempt to bypass
|
||||
throw new Error('A recovery email is required by Microsoft but not configured in config.json');
|
||||
}
|
||||
|
||||
// If an email address is configured and an email option is detected
|
||||
if (this.bot.config.recoveryEmail && this.bot.config.recoveryEmail.trim() !== '') {
|
||||
const configuredEmail = this.bot.config.recoveryEmail.trim();
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `→ Using configured recovery email: ${configuredEmail.substring(0, 3)}...${configuredEmail.split('@')[1]}`, 'log', 'green');
|
||||
|
||||
// Search for an email input field (more selectors for better detection)
|
||||
const emailInput = await page.waitForSelector(
|
||||
'input[type="email"], input[id*="email"], input[name*="email"], input[placeholder*="email"], input[aria-label*="email"]',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (emailInput) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Email input field detected', 'log', 'green');
|
||||
|
||||
await emailInput.fill('');
|
||||
await this.bot.utils.wait(500);
|
||||
await emailInput.fill(configuredEmail);
|
||||
await this.bot.utils.wait(1000);
|
||||
|
||||
// Expanded list of selectors for the next/validate button
|
||||
const nextButton = await page.waitForSelector(
|
||||
'button:has-text("Next"), ' +
|
||||
'button:has-text("Save"), ' +
|
||||
'button:has-text("Continue"), ' +
|
||||
'button:has-text("Verify"), ' +
|
||||
'button:has-text("Add"), ' +
|
||||
'button[type="submit"], input[type="submit"]',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null);
|
||||
|
||||
if (nextButton) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Validation button found, submitting email', 'log', 'green');
|
||||
await nextButton.click();
|
||||
await this.bot.utils.wait(2000);
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ Recovery email submitted successfully', 'log', 'green');
|
||||
return;
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ ERROR: Validation button not found after entering email', 'log', 'red');
|
||||
throw new Error('Validation button not found after entering recovery email');
|
||||
}
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '→ ERROR: No email field found despite detection of protection screen', 'log', 'red');
|
||||
throw new Error('Email field not found in account protection screen');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'No account protection screen detected', 'log', 'green');
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Account protection handling error: ${error}`, 'error');
|
||||
throw error; // Re-throw the error to stop execution
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current state of the login process and handles accordingly
|
||||
* This helps when the browser is reopened and we're already at a specific step of the login process
|
||||
*/
|
||||
private async checkCurrentLoginState(page: Page, email: string, password: string) {
|
||||
try {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Checking current login state...')
|
||||
|
||||
// Check if we're on the recovery email page
|
||||
const recoveryEmailInput = await page.waitForSelector(
|
||||
'input[type="email"][aria-label*="recovery"], input[name="RecoveryEmailAddress"], #RecoveryEmailAddress, input[placeholder*="recovery"], input[type="email"][id*="recovery"]',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (recoveryEmailInput) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Detected session resumed at recovery email page', 'log', 'yellow')
|
||||
await this.handleRecoveryEmail(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're on the password page (already entered email)
|
||||
const passwordInput = await page.waitForSelector(
|
||||
'input[type="password"], input[name="passwd"], input[id="i0118"]',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (passwordInput) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Detected session resumed at password page', 'log', 'yellow')
|
||||
await this.enterPassword(page, password)
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.handleRecoveryEmail(page)
|
||||
await this.handleStaySignedIn(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're at 2FA page
|
||||
const twoFactorElement = await page.waitForSelector(
|
||||
'#displaySign, #idRichContext_DisplaySign',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (twoFactorElement) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Detected session resumed at 2FA verification page', 'log', 'yellow')
|
||||
await this.handle2FA(page)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we're at the "Stay signed in" page
|
||||
const staySignedInPrompt = await page.waitForSelector(
|
||||
'#KmsiCheckboxField, input[name="DontShowAgain"], [role="heading"]:has-text("Stay signed in?")',
|
||||
{ timeout: 3000 }
|
||||
).catch(() => null)
|
||||
|
||||
if (staySignedInPrompt) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Detected session resumed at "Stay signed in" page', 'log', 'yellow')
|
||||
await this.handleStaySignedIn(page)
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'No specific login state detected, proceeding with normal login flow')
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Error checking login state: ${error}`, 'warn')
|
||||
}
|
||||
}
|
||||
}
|
||||
230
src/functions/Workers.ts
Normal file
230
src/functions/Workers.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
export class Workers {
|
||||
public bot: MicrosoftRewardsBot
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
// Daily Set
|
||||
async doDailySet(page: Page, data: DashboardData) {
|
||||
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
|
||||
|
||||
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
|
||||
|
||||
if (!activitiesUncompleted.length) {
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
|
||||
return
|
||||
}
|
||||
|
||||
// Solve Activities
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'Started solving "Daily Set" items')
|
||||
|
||||
await this.solveActivities(page, activitiesUncompleted)
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
// Always return to the homepage if not already
|
||||
await this.bot.browser.func.goHome(page)
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have been completed')
|
||||
}
|
||||
|
||||
// Punch Card
|
||||
async doPunchCard(page: Page, data: DashboardData) {
|
||||
|
||||
const punchCardsUncompleted = data.punchCards?.filter(x => x.parentPromotion && !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
|
||||
|
||||
if (!punchCardsUncompleted.length) {
|
||||
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Cards" have already been completed')
|
||||
return
|
||||
}
|
||||
|
||||
for (const punchCard of punchCardsUncompleted) {
|
||||
|
||||
// Ensure parentPromotion exists before proceeding
|
||||
if (!punchCard.parentPromotion?.title) {
|
||||
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Skipped punchcard "${punchCard.name}" | Reason: Parent promotion is missing!`, 'warn')
|
||||
continue
|
||||
}
|
||||
|
||||
// Get latest page for each card
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
|
||||
|
||||
// Solve Activities
|
||||
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
|
||||
|
||||
// Got to punch card index page in a new tab
|
||||
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
|
||||
|
||||
// Wait for new page to load, max 10 seconds, however try regardless in case of error
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
|
||||
|
||||
await this.solveActivities(page, activitiesUncompleted, punchCard)
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
const pages = page.context().pages()
|
||||
|
||||
if (pages.length > 3) {
|
||||
await page.close()
|
||||
} else {
|
||||
await this.bot.browser.func.goHome(page)
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Card" items have been completed')
|
||||
}
|
||||
|
||||
// More Promotions
|
||||
async doMorePromotions(page: Page, data: DashboardData) {
|
||||
const morePromotions = data.morePromotions
|
||||
|
||||
// Check if there is a promotional item
|
||||
if (data.promotionalItem) { // Convert and add the promotional item to the array
|
||||
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
|
||||
}
|
||||
|
||||
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && x.exclusiveLockedFeatureStatus !== 'locked') ?? []
|
||||
|
||||
if (!activitiesUncompleted.length) {
|
||||
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
|
||||
return
|
||||
}
|
||||
|
||||
// Solve Activities
|
||||
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'Started solving "More Promotions" items')
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
await this.solveActivities(page, activitiesUncompleted)
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
// Always return to the homepage if not already
|
||||
await this.bot.browser.func.goHome(page)
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
|
||||
}
|
||||
|
||||
// Solve all the different types of activities
|
||||
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
|
||||
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
|
||||
|
||||
for (const activity of activities) {
|
||||
try {
|
||||
// Reselect the worker page
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
|
||||
const pages = activityPage.context().pages()
|
||||
if (pages.length > 3) {
|
||||
await activityPage.close()
|
||||
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
if (activityPage.url() !== activityInitial) {
|
||||
await activityPage.goto(activityInitial)
|
||||
}
|
||||
|
||||
|
||||
let selector = `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
|
||||
|
||||
if (punchCard) {
|
||||
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
|
||||
|
||||
} else if (activity.name.toLowerCase().includes('membercenter') || activity.name.toLowerCase().includes('exploreonbing')) {
|
||||
selector = `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
|
||||
}
|
||||
|
||||
// Wait for the new tab to fully load, ignore error.
|
||||
/*
|
||||
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
|
||||
if it didn't then it gave enough time for the page to load.
|
||||
*/
|
||||
await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
switch (activity.promotionType) {
|
||||
// Quiz (Poll, Quiz or ABC)
|
||||
case 'quiz':
|
||||
switch (activity.pointProgressMax) {
|
||||
// Poll or ABC (Usually 10 points)
|
||||
case 10:
|
||||
// Normal poll
|
||||
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doPoll(activityPage)
|
||||
} else { // ABC
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doABC(activityPage)
|
||||
}
|
||||
break
|
||||
|
||||
// This Or That Quiz (Usually 50 points)
|
||||
case 50:
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doThisOrThat(activityPage)
|
||||
break
|
||||
|
||||
// Quizzes are usually 30-40 points
|
||||
default:
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doQuiz(activityPage)
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
// UrlReward (Visit)
|
||||
case 'urlreward':
|
||||
// Search on Bing are subtypes of "urlreward"
|
||||
if (activity.name.toLowerCase().includes('exploreonbing')) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "SearchOnBing" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doSearchOnBing(activityPage, activity)
|
||||
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
|
||||
await activityPage.click(selector)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
await this.bot.activities.doUrlReward(activityPage)
|
||||
}
|
||||
break
|
||||
|
||||
// Unsupported types
|
||||
default:
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
|
||||
break
|
||||
}
|
||||
|
||||
// Cooldown
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
52
src/functions/activities/ABC.ts
Normal file
52
src/functions/activities/ABC.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
|
||||
export class ABC extends Workers {
|
||||
|
||||
async doABC(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'ABC', 'Trying to complete poll')
|
||||
|
||||
try {
|
||||
let $ = await this.bot.browser.func.loadInCheerio(page)
|
||||
|
||||
// Don't loop more than 15 in case unable to solve, would lock otherwise
|
||||
const maxIterations = 15
|
||||
let i
|
||||
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
|
||||
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10000 })
|
||||
|
||||
const answers = $('.wk_OptionClickClass')
|
||||
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
|
||||
|
||||
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10000 })
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
await page.click(`#${answer}`) // Click answer
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10000 })
|
||||
await page.click('div.wk_button') // Click next question button
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
$ = await this.bot.browser.func.loadInCheerio(page)
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
await page.close()
|
||||
|
||||
if (i === maxIterations) {
|
||||
this.bot.log(this.bot.isMobile, 'ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'ABC', 'Completed the ABC successfully')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'ABC', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
src/functions/activities/DailyCheckIn.ts
Normal file
48
src/functions/activities/DailyCheckIn.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { DashboardData } from '../../interface/DashboardData'
|
||||
|
||||
|
||||
export class DailyCheckIn extends Workers {
|
||||
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'Starting Daily Check In')
|
||||
|
||||
try {
|
||||
let geoLocale = data.userProfile.attributes.country
|
||||
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||
|
||||
const jsonData = {
|
||||
amount: 1,
|
||||
country: geoLocale,
|
||||
id: randomBytes(64).toString('hex'),
|
||||
type: 101,
|
||||
attributes: {
|
||||
offerid: 'Gamification_Sapphire_DailyCheckIn'
|
||||
}
|
||||
}
|
||||
|
||||
const claimRequest: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
const claimResponse = await this.bot.axios.request(claimRequest)
|
||||
const claimedPoint = parseInt((await claimResponse.data).response?.activity?.p) ?? 0
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
src/functions/activities/Poll.ts
Normal file
29
src/functions/activities/Poll.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
|
||||
export class Poll extends Workers {
|
||||
|
||||
async doPoll(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'POLL', 'Trying to complete poll')
|
||||
|
||||
try {
|
||||
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
|
||||
|
||||
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10000 }).catch(() => { }) // We're gonna click regardless or not
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
await page.click(buttonId)
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
await page.close()
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'POLL', 'Completed the poll successfully')
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'POLL', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
92
src/functions/activities/Quiz.ts
Normal file
92
src/functions/activities/Quiz.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
|
||||
export class Quiz extends Workers {
|
||||
|
||||
async doQuiz(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'Trying to complete quiz')
|
||||
|
||||
try {
|
||||
// Check if the quiz has been started or not
|
||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (quizNotStarted) {
|
||||
await page.click('#rqStartQuiz')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz has already been started, trying to finish it')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
let quizData = await this.bot.browser.func.getQuizData(page)
|
||||
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
|
||||
|
||||
// All questions
|
||||
for (let question = 0; question < questionsRemaining; question++) {
|
||||
|
||||
if (quizData.numberOfOptions === 8) {
|
||||
const answers: string[] = []
|
||||
|
||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
|
||||
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
|
||||
|
||||
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
|
||||
answers.push(`#rqAnswerOption${i}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Click the answers
|
||||
for (const answer of answers) {
|
||||
await page.waitForSelector(answer, { state: 'visible', timeout: 2000 })
|
||||
|
||||
// Click the answer on page
|
||||
await page.click(answer)
|
||||
|
||||
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
|
||||
if (!refreshSuccess) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Other type quiz, lightspeed
|
||||
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
|
||||
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
|
||||
const correctOption = quizData.correctAnswer
|
||||
|
||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
||||
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
|
||||
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
|
||||
|
||||
if (dataOption === correctOption) {
|
||||
// Click the answer on page
|
||||
await page.click(`#rqAnswerOption${i}`)
|
||||
|
||||
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
|
||||
if (!refreshSuccess) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.bot.utils.wait(2000)
|
||||
}
|
||||
}
|
||||
|
||||
// Done with
|
||||
await this.bot.utils.wait(2000)
|
||||
await page.close()
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'Completed the quiz successfully')
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
73
src/functions/activities/ReadToEarn.ts
Normal file
73
src/functions/activities/ReadToEarn.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { DashboardData } from '../../interface/DashboardData'
|
||||
|
||||
|
||||
export class ReadToEarn extends Workers {
|
||||
public async doReadToEarn(accessToken: string, data: DashboardData) {
|
||||
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Starting Read to Earn')
|
||||
|
||||
try {
|
||||
let geoLocale = data.userProfile.attributes.country
|
||||
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||
|
||||
const userDataRequest: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
}
|
||||
}
|
||||
const userDataResponse = await this.bot.axios.request(userDataRequest)
|
||||
const userData = (await userDataResponse.data).response
|
||||
let userBalance = userData.balance
|
||||
|
||||
const jsonData = {
|
||||
amount: 1,
|
||||
country: geoLocale,
|
||||
id: '1',
|
||||
type: 101,
|
||||
attributes: {
|
||||
offerid: 'ENUS_readarticle3_30points'
|
||||
}
|
||||
}
|
||||
|
||||
const articleCount = 10
|
||||
for (let i = 0; i < articleCount; ++i) {
|
||||
jsonData.id = randomBytes(64).toString('hex')
|
||||
const claimRequest = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Rewards-Country': geoLocale,
|
||||
'X-Rewards-Language': 'en'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
const claimResponse = await this.bot.axios.request(claimRequest)
|
||||
const newBalance = (await claimResponse.data).response.balance
|
||||
|
||||
if (newBalance == userBalance) {
|
||||
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Read all available articles')
|
||||
break
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', `Read article ${i + 1} of ${articleCount} max | Gained ${newBalance - userBalance} Points`)
|
||||
userBalance = newBalance
|
||||
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Completed Read to Earn')
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
400
src/functions/activities/Search.ts
Normal file
400
src/functions/activities/Search.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { platform } from 'os'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { Counters, DashboardData } from '../../interface/DashboardData'
|
||||
import { GoogleSearch } from '../../interface/Search'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
|
||||
type GoogleTrendsResponse = [
|
||||
string,
|
||||
[
|
||||
string,
|
||||
...null[],
|
||||
[string, ...string[]]
|
||||
][]
|
||||
];
|
||||
|
||||
export class Search extends Workers {
|
||||
private bingHome = 'https://bing.com'
|
||||
private searchPageURL = ''
|
||||
|
||||
public async doSearch(page: Page, data: DashboardData) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Starting Bing searches')
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
|
||||
let missingPoints = this.calculatePoints(searchCounters)
|
||||
|
||||
if (missingPoints === 0) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Bing searches have already been completed')
|
||||
return
|
||||
}
|
||||
|
||||
// Generate search queries
|
||||
let googleSearchQueries = await this.getGoogleTrends(this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US')
|
||||
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
|
||||
|
||||
// Deduplicate the search terms
|
||||
googleSearchQueries = [...new Set(googleSearchQueries)]
|
||||
|
||||
// Go to bing
|
||||
await page.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it doesn't continue after 5 more searches with alternative queries, abort search
|
||||
|
||||
const queries: string[] = []
|
||||
// Mobile search doesn't seem to like related queries?
|
||||
googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) })
|
||||
|
||||
// Loop over Google search queries
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
const query = queries[i] as string
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query}`)
|
||||
|
||||
searchCounters = await this.bingSearch(page, query)
|
||||
const newMissingPoints = this.calculatePoints(searchCounters)
|
||||
|
||||
// If the new point amount is the same as before
|
||||
if (newMissingPoints == missingPoints) {
|
||||
maxLoop++ // Add to max loop
|
||||
} else { // There has been a change in points
|
||||
maxLoop = 0 // Reset the loop
|
||||
}
|
||||
|
||||
missingPoints = newMissingPoints
|
||||
|
||||
if (missingPoints === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
// Only for mobile searches
|
||||
if (maxLoop > 5 && this.bot.isMobile) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
|
||||
break
|
||||
}
|
||||
|
||||
// If we didn't gain points for 10 iterations, assume it's stuck
|
||||
if (maxLoop > 10) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
|
||||
maxLoop = 0 // Reset to 0 so we can retry with related searches below
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Only for mobile searches
|
||||
if (missingPoints > 0 && this.bot.isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we still got remaining search queries, generate extra ones
|
||||
if (missingPoints > 0) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
|
||||
|
||||
let i = 0
|
||||
while (missingPoints > 0) {
|
||||
const query = googleSearchQueries[i++] as GoogleSearch
|
||||
|
||||
// Get related search terms to the Google search queries
|
||||
const relatedTerms = await this.getRelatedTerms(query?.topic)
|
||||
if (relatedTerms.length > 3) {
|
||||
// Search for the first 2 related terms
|
||||
for (const term of relatedTerms.slice(1, 3)) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term}`)
|
||||
|
||||
searchCounters = await this.bingSearch(page, term)
|
||||
const newMissingPoints = this.calculatePoints(searchCounters)
|
||||
|
||||
// If the new point amount is the same as before
|
||||
if (newMissingPoints == missingPoints) {
|
||||
maxLoop++ // Add to max loop
|
||||
} else { // There has been a change in points
|
||||
maxLoop = 0 // Reset the loop
|
||||
}
|
||||
|
||||
missingPoints = newMissingPoints
|
||||
|
||||
// If we satisfied the searches
|
||||
if (missingPoints === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
|
||||
if (maxLoop > 5) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Completed searches')
|
||||
}
|
||||
|
||||
private async bingSearch(searchPage: Page, query: string) {
|
||||
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
|
||||
|
||||
// Try a max of 5 times
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
// This page had already been set to the Bing.com page or the previous search listing, we just need to select it
|
||||
searchPage = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||
|
||||
// Go to top of the page
|
||||
await searchPage.evaluate(() => {
|
||||
window.scrollTo(0, 0)
|
||||
})
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
|
||||
const searchBar = '#sb_form_q'
|
||||
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
|
||||
await searchPage.click(searchBar) // Focus on the textarea
|
||||
await this.bot.utils.wait(500)
|
||||
await searchPage.keyboard.down(platformControlKey)
|
||||
await searchPage.keyboard.press('A')
|
||||
await searchPage.keyboard.press('Backspace')
|
||||
await searchPage.keyboard.up(platformControlKey)
|
||||
await searchPage.keyboard.type(query)
|
||||
await searchPage.keyboard.press('Enter')
|
||||
|
||||
await this.bot.utils.wait(3000)
|
||||
|
||||
// Bing.com in Chrome opens a new tab when searching
|
||||
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||
this.searchPageURL = new URL(resultPage.url()).href // Set the results page
|
||||
|
||||
await this.bot.browser.utils.reloadBadPage(resultPage)
|
||||
|
||||
if (this.bot.config.searchSettings.scrollRandomResults) {
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.randomScroll(resultPage)
|
||||
}
|
||||
|
||||
if (this.bot.config.searchSettings.clickRandomResults) {
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.clickRandomLink(resultPage)
|
||||
}
|
||||
|
||||
// Delay between searches
|
||||
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
|
||||
|
||||
return await this.bot.browser.func.getSearchPoints()
|
||||
|
||||
} catch (error) {
|
||||
if (i === 5) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
|
||||
|
||||
// Reset the tabs
|
||||
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||
await this.closeTabs(lastTab)
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
|
||||
return await this.bot.browser.func.getSearchPoints()
|
||||
}
|
||||
|
||||
private async getGoogleTrends(geoLocale: string = 'US'): Promise<GoogleSearch[]> {
|
||||
const queryTerms: GoogleSearch[] = []
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
|
||||
|
||||
try {
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://trends.google.com/_/TrendsUi/data/batchexecute',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale.toUpperCase()}\\", 0, null, 48]"]]]`
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
|
||||
const rawText = response.data
|
||||
|
||||
const trendsData = this.extractJsonFromResponse(rawText)
|
||||
if (!trendsData) {
|
||||
throw this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
|
||||
}
|
||||
|
||||
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
|
||||
if (mappedTrendsData.length < 90) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Insufficient search queries, falling back to US', 'warn')
|
||||
return this.getGoogleTrends()
|
||||
}
|
||||
|
||||
for (const [topic, relatedQueries] of mappedTrendsData) {
|
||||
queryTerms.push({
|
||||
topic: topic as string,
|
||||
related: relatedQueries as string[]
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
return queryTerms
|
||||
}
|
||||
|
||||
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||
try {
|
||||
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async getRelatedTerms(term: string): Promise<string[]> {
|
||||
try {
|
||||
const request = {
|
||||
url: `https://api.bing.com/osjson.aspx?query=${term}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyBingTerms)
|
||||
|
||||
return response.data[1] as string[]
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-BING-RELATED', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
private async randomScroll(page: Page) {
|
||||
try {
|
||||
const viewportHeight = await page.evaluate(() => window.innerHeight)
|
||||
const totalHeight = await page.evaluate(() => document.body.scrollHeight)
|
||||
const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
|
||||
|
||||
await page.evaluate((scrollPos) => {
|
||||
window.scrollTo(0, scrollPos)
|
||||
}, randomScrollPosition)
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async clickRandomLink(page: Page) {
|
||||
try {
|
||||
await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
|
||||
|
||||
// Only used if the browser is not the edge browser (continue on Edge popup)
|
||||
await this.closeContinuePopup(page)
|
||||
|
||||
// Stay for 10 seconds for page to load and "visit"
|
||||
await this.bot.utils.wait(10000)
|
||||
|
||||
// Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
|
||||
let lastTab = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
let lastTabURL = new URL(lastTab.url()) // Get new tab info, this is the website we're visiting
|
||||
|
||||
// Check if the URL is different from the original one, don't loop more than 5 times.
|
||||
let i = 0
|
||||
while (lastTabURL.href !== this.searchPageURL && i < 5) {
|
||||
|
||||
await this.closeTabs(lastTab)
|
||||
|
||||
// End of loop, refresh lastPage
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
|
||||
lastTabURL = new URL(lastTab.url()) // Get new tab info
|
||||
i++
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async closeTabs(lastTab: Page) {
|
||||
const browser = lastTab.context()
|
||||
const tabs = browser.pages()
|
||||
|
||||
try {
|
||||
if (tabs.length > 2) {
|
||||
// If more than 2 tabs are open, close the last tab
|
||||
|
||||
await lastTab.close()
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', `More than 2 were open, closed the last tab: "${new URL(lastTab.url()).host}"`)
|
||||
|
||||
} else if (tabs.length === 1) {
|
||||
// If only 1 tab is open, open a new one to search in
|
||||
|
||||
const newPage = await browser.newPage()
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
await newPage.goto(this.bingHome)
|
||||
await this.bot.utils.wait(3000)
|
||||
this.searchPageURL = newPage.url()
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'There was only 1 tab open, crated a new one')
|
||||
} else {
|
||||
// Else reset the last tab back to the search listing or Bing.com
|
||||
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(lastTab)
|
||||
await lastTab.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private calculatePoints(counters: Counters) {
|
||||
const mobileData = counters.mobileSearch?.[0] // Mobile searches
|
||||
const genericData = counters.pcSearch?.[0] // Normal searches
|
||||
const edgeData = counters.pcSearch?.[1] // Edge searches
|
||||
|
||||
const missingPoints = (this.bot.isMobile && mobileData)
|
||||
? mobileData.pointProgressMax - mobileData.pointProgress
|
||||
: (edgeData ? edgeData.pointProgressMax - edgeData.pointProgress : 0)
|
||||
+ (genericData ? genericData.pointProgressMax - genericData.pointProgress : 0)
|
||||
|
||||
return missingPoints
|
||||
}
|
||||
|
||||
private async closeContinuePopup(page: Page) {
|
||||
try {
|
||||
await page.waitForSelector('#sacs_close', { timeout: 1000 })
|
||||
const continueButton = await page.$('#sacs_close')
|
||||
|
||||
if (continueButton) {
|
||||
await continueButton.click()
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue if element is not found or other error occurs
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
75
src/functions/activities/SearchOnBing.ts
Normal file
75
src/functions/activities/SearchOnBing.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { MorePromotion, PromotionalItem } from '../../interface/DashboardData'
|
||||
|
||||
|
||||
export class SearchOnBing extends Workers {
|
||||
|
||||
async doSearchOnBing(page: Page, activity: MorePromotion | PromotionalItem) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Trying to complete SearchOnBing')
|
||||
|
||||
try {
|
||||
await this.bot.utils.wait(5000)
|
||||
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
const query = await this.getSearchQuery(activity.title)
|
||||
|
||||
const searchBar = '#sb_form_q'
|
||||
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
|
||||
await page.click(searchBar)
|
||||
await this.bot.utils.wait(500)
|
||||
await page.keyboard.type(query)
|
||||
await page.keyboard.press('Enter')
|
||||
await this.bot.utils.wait(3000)
|
||||
|
||||
await page.close()
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Completed the SearchOnBing successfully')
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
private async getSearchQuery(title: string): Promise<string> {
|
||||
interface Queries {
|
||||
title: string;
|
||||
queries: string[]
|
||||
}
|
||||
|
||||
let queries: Queries[] = []
|
||||
|
||||
try {
|
||||
if (this.bot.config.searchOnBingLocalQueries) {
|
||||
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8')
|
||||
queries = JSON.parse(data)
|
||||
} else {
|
||||
// Fetch from the repo directly so the user doesn't need to redownload the script for the new activities
|
||||
const response = await this.bot.axios.request({
|
||||
method: 'GET',
|
||||
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/main/src/functions/queries.json'
|
||||
})
|
||||
queries = response.data
|
||||
}
|
||||
|
||||
const answers = queries.find(x => this.normalizeString(x.title) === this.normalizeString(title))
|
||||
const answer = answers ? this.bot.utils.shuffleArray(answers?.queries)[0] as string : title
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', `Fetched answer: ${answer} | question: ${title}`)
|
||||
return answer
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'An error occurred:' + error, 'error')
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeString(string: string): string {
|
||||
return string.normalize('NFD').trim().toLowerCase().replace(/[^\x20-\x7E]/g, '').replace(/[?!]/g, '')
|
||||
}
|
||||
}
|
||||
47
src/functions/activities/ThisOrThat.ts
Normal file
47
src/functions/activities/ThisOrThat.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
|
||||
export class ThisOrThat extends Workers {
|
||||
|
||||
async doThisOrThat(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Trying to complete ThisOrThat')
|
||||
|
||||
|
||||
try {
|
||||
// Check if the quiz has been started or not
|
||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (quizNotStarted) {
|
||||
await page.click('#rqStartQuiz')
|
||||
} else {
|
||||
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
// Solving
|
||||
const quizData = await this.bot.browser.func.getQuizData(page)
|
||||
const questionsRemaining = quizData.maxQuestions - (quizData.currentQuestionNumber - 1) // Amount of questions remaining
|
||||
|
||||
for (let question = 0; question < questionsRemaining; question++) {
|
||||
// Since there's no solving logic yet, randomly guess to complete
|
||||
const buttonId = `#rqAnswerOption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
|
||||
await page.click(buttonId)
|
||||
|
||||
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
|
||||
if (!refreshSuccess) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Completed the ThisOrThat successfully')
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
23
src/functions/activities/UrlReward.ts
Normal file
23
src/functions/activities/UrlReward.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
|
||||
export class UrlReward extends Workers {
|
||||
|
||||
async doUrlReward(page: Page) {
|
||||
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Trying to complete UrlReward')
|
||||
|
||||
try {
|
||||
this.bot.utils.wait(2000)
|
||||
|
||||
await page.close()
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Completed the UrlReward successfully')
|
||||
} catch (error) {
|
||||
await page.close()
|
||||
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
289
src/functions/queries.json
Normal file
289
src/functions/queries.json
Normal file
@@ -0,0 +1,289 @@
|
||||
[
|
||||
{
|
||||
"title": "Houses near you",
|
||||
"queries": [
|
||||
"Houses near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Feeling symptoms?",
|
||||
"queries": [
|
||||
"Rash on forearm",
|
||||
"Stuffy nose",
|
||||
"Tickling cough"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Get your shopping done faster",
|
||||
"queries": [
|
||||
"Buy PS5",
|
||||
"Buy Xbox",
|
||||
"Chair deals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Translate anything",
|
||||
"queries": [
|
||||
"Translate welcome home to Korean",
|
||||
"Translate welcome home to Japanese",
|
||||
"Translate goodbye to Japanese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Search the lyrics of a song",
|
||||
"queries": [
|
||||
"Debarge rhythm of the night lyrics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Let's watch that movie again!",
|
||||
"queries": [
|
||||
"Alien movie",
|
||||
"Aliens movie",
|
||||
"Alien 3 movie",
|
||||
"Predator movie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Plan a quick getaway",
|
||||
"queries": [
|
||||
"Flights Amsterdam to Tokyo",
|
||||
"Flights New York to Tokyo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Discover open job roles",
|
||||
"queries": [
|
||||
"jobs at Microsoft",
|
||||
"Microsoft Job Openings",
|
||||
"Jobs near me",
|
||||
"jobs at Boeing worked"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "You can track your package",
|
||||
"queries": [
|
||||
"USPS tracking"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Find somewhere new to explore",
|
||||
"queries": [
|
||||
"Directions to Berlin",
|
||||
"Directions to Tokyo",
|
||||
"Directions to New York"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Too tired to cook tonight?",
|
||||
"queries": [
|
||||
"KFC near me",
|
||||
"Burger King near me",
|
||||
"McDonalds near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Quickly convert your money",
|
||||
"queries": [
|
||||
"convert 250 USD to yen",
|
||||
"convert 500 USD to yen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Learn to cook a new recipe",
|
||||
"queries": [
|
||||
"How to cook ratatouille",
|
||||
"How to cook lasagna"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Find places to stay!",
|
||||
"queries": [
|
||||
"Hotels Berlin Germany",
|
||||
"Hotels Amsterdam Netherlands"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "How's the economy?",
|
||||
"queries": [
|
||||
"sp 500"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Who won?",
|
||||
"queries": [
|
||||
"braves score"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Gaming time",
|
||||
"queries": [
|
||||
"Overwatch video game",
|
||||
"Call of duty video game"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Expand your vocabulary",
|
||||
"queries": [
|
||||
"definition definition"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "What time is it?",
|
||||
"queries": [
|
||||
"Japan time",
|
||||
"New York time"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Maisons près de chez vous",
|
||||
"queries": [
|
||||
"Maisons près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vous ressentez des symptômes ?",
|
||||
"queries": [
|
||||
"Éruption cutanée sur l'avant-bras",
|
||||
"Nez bouché",
|
||||
"Toux chatouilleuse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Faites vos achats plus vite",
|
||||
"queries": [
|
||||
"Acheter une PS5",
|
||||
"Acheter une Xbox",
|
||||
"Offres sur les chaises"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Traduisez tout !",
|
||||
"queries": [
|
||||
"Traduction bienvenue à la maison en coréen",
|
||||
"Traduction bienvenue à la maison en japonais",
|
||||
"Traduction au revoir en japonais"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Rechercher paroles de chanson",
|
||||
"queries": [
|
||||
"Paroles de Debarge rhythm of the night"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Et si nous regardions ce film une nouvelle fois?",
|
||||
"queries": [
|
||||
"Alien film",
|
||||
"Film Aliens",
|
||||
"Film Alien 3",
|
||||
"Film Predator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Planifiez une petite escapade",
|
||||
"queries": [
|
||||
"Vols Amsterdam-Tokyo",
|
||||
"Vols New York-Tokyo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Consulter postes à pourvoir",
|
||||
"queries": [
|
||||
"emplois chez Microsoft",
|
||||
"Offres d'emploi Microsoft",
|
||||
"Emplois près de chez moi",
|
||||
"emplois chez Boeing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vous pouvez suivre votre colis",
|
||||
"queries": [
|
||||
"Suivi Chronopost"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trouver un endroit à découvrir",
|
||||
"queries": [
|
||||
"Itinéraire vers Berlin",
|
||||
"Itinéraire vers Tokyo",
|
||||
"Itinéraire vers New York"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trop fatigué pour cuisiner ce soir ?",
|
||||
"queries": [
|
||||
"KFC près de chez moi",
|
||||
"Burger King près de chez moi",
|
||||
"McDonalds près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Convertissez rapidement votre argent",
|
||||
"queries": [
|
||||
"convertir 250 EUR en yen",
|
||||
"convertir 500 EUR en yen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Apprenez à cuisiner une nouvelle recette",
|
||||
"queries": [
|
||||
"Comment faire cuire la ratatouille",
|
||||
"Comment faire cuire les lasagnes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trouvez des emplacements pour rester!",
|
||||
"queries": [
|
||||
"Hôtels Berlin Allemagne",
|
||||
"Hôtels Amsterdam Pays-Bas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Comment se porte l'économie ?",
|
||||
"queries": [
|
||||
"CAC 40"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Qui a gagné ?",
|
||||
"queries": [
|
||||
"score du Paris Saint-Germain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Temps de jeu",
|
||||
"queries": [
|
||||
"Jeu vidéo Overwatch",
|
||||
"Jeu vidéo Call of Duty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Enrichissez votre vocabulaire",
|
||||
"queries": [
|
||||
"definition definition"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Quelle heure est-il ?",
|
||||
"queries": [
|
||||
"Heure du Japon",
|
||||
"Heure de New York"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vérifier la météo",
|
||||
"queries": [
|
||||
"Météo de Paris",
|
||||
"Météo de la France"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Tenez-vous informé des sujets d'actualité",
|
||||
"queries": [
|
||||
"Augmentation Impots",
|
||||
"Mort célébrité"
|
||||
]
|
||||
}
|
||||
]
|
||||
319
src/index.ts
Normal file
319
src/index.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import cluster from 'cluster'
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
|
||||
import Browser from './browser/Browser'
|
||||
import BrowserFunc from './browser/BrowserFunc'
|
||||
import BrowserUtil from './browser/BrowserUtil'
|
||||
|
||||
import { log } from './util/Logger'
|
||||
import Util from './util/Utils'
|
||||
import { loadAccounts, loadConfig, saveSessionData } from './util/Load'
|
||||
|
||||
import { Login } from './functions/Login'
|
||||
import { Workers } from './functions/Workers'
|
||||
import Activities from './functions/Activities'
|
||||
|
||||
import { Account } from './interface/Account'
|
||||
import Axios from './util/Axios'
|
||||
|
||||
|
||||
// Main bot class
|
||||
export class MicrosoftRewardsBot {
|
||||
public log: typeof log
|
||||
public config
|
||||
public utils: Util
|
||||
public activities: Activities = new Activities(this)
|
||||
public browser: {
|
||||
func: BrowserFunc,
|
||||
utils: BrowserUtil
|
||||
}
|
||||
public isMobile: boolean
|
||||
public homePage!: Page
|
||||
|
||||
private pointsCanCollect: number = 0
|
||||
private pointsInitial: number = 0
|
||||
|
||||
private activeWorkers: number
|
||||
private mobileRetryAttempts: number
|
||||
private browserFactory: Browser = new Browser(this)
|
||||
private accounts: Account[]
|
||||
private workers: Workers
|
||||
private login = new Login(this)
|
||||
private accessToken: string = ''
|
||||
|
||||
//@ts-expect-error Will be initialized later
|
||||
public axios: Axios
|
||||
|
||||
constructor(isMobile: boolean) {
|
||||
this.isMobile = isMobile
|
||||
this.log = log
|
||||
|
||||
this.accounts = []
|
||||
this.utils = new Util()
|
||||
this.workers = new Workers(this)
|
||||
this.browser = {
|
||||
func: new BrowserFunc(this),
|
||||
utils: new BrowserUtil(this)
|
||||
}
|
||||
this.config = loadConfig()
|
||||
this.activeWorkers = this.config.clusters
|
||||
this.mobileRetryAttempts = 0
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.accounts = loadAccounts()
|
||||
}
|
||||
|
||||
async run() {
|
||||
log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`)
|
||||
|
||||
// Only cluster when there's more than 1 cluster demanded
|
||||
if (this.config.clusters > 1) {
|
||||
if (cluster.isPrimary) {
|
||||
this.runMaster()
|
||||
} else {
|
||||
this.runWorker()
|
||||
}
|
||||
} else {
|
||||
await this.runTasks(this.accounts)
|
||||
}
|
||||
}
|
||||
|
||||
private runMaster() {
|
||||
log('main', 'MAIN-PRIMARY', 'Primary process started')
|
||||
|
||||
const accountChunks = this.utils.chunkArray(this.accounts, this.config.clusters)
|
||||
|
||||
for (let i = 0; i < accountChunks.length; i++) {
|
||||
const worker = cluster.fork()
|
||||
const chunk = accountChunks[i]
|
||||
worker.send({ chunk })
|
||||
}
|
||||
|
||||
cluster.on('exit', (worker, code) => {
|
||||
this.activeWorkers -= 1
|
||||
|
||||
log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn')
|
||||
|
||||
// Check if all workers have exited
|
||||
if (this.activeWorkers === 0) {
|
||||
log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
|
||||
process.exit(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private runWorker() {
|
||||
log('main', 'MAIN-WORKER', `Worker ${process.pid} spawned`)
|
||||
// Receive the chunk of accounts from the master
|
||||
process.on('message', async ({ chunk }) => {
|
||||
await this.runTasks(chunk)
|
||||
})
|
||||
}
|
||||
|
||||
private async runTasks(accounts: Account[]) {
|
||||
for (const account of accounts) {
|
||||
log('main', 'MAIN-WORKER', `Started tasks for account ${account.email}`)
|
||||
|
||||
this.axios = new Axios(account.proxy)
|
||||
if (this.config.parallel) {
|
||||
await Promise.all([
|
||||
this.Desktop(account),
|
||||
(() => {
|
||||
const mobileInstance = new MicrosoftRewardsBot(true)
|
||||
mobileInstance.axios = this.axios
|
||||
|
||||
return mobileInstance.Mobile(account)
|
||||
})()
|
||||
])
|
||||
} else {
|
||||
this.isMobile = false
|
||||
await this.Desktop(account)
|
||||
|
||||
this.isMobile = true
|
||||
await this.Mobile(account)
|
||||
}
|
||||
|
||||
log('main', 'MAIN-WORKER', `Completed tasks for account ${account.email}`, 'log', 'green')
|
||||
}
|
||||
|
||||
log(this.isMobile, 'MAIN-PRIMARY', 'Completed tasks for ALL accounts', 'log', 'green')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
// Desktop
|
||||
async Desktop(account: Account) {
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
log(this.isMobile, 'MAIN', 'Starting browser')
|
||||
|
||||
// Login into MS Rewards, then go to rewards homepage
|
||||
await this.login.login(this.homePage, account.email, account.password)
|
||||
|
||||
await this.browser.func.goHome(this.homePage)
|
||||
|
||||
const data = await this.browser.func.getDashboardData()
|
||||
|
||||
this.pointsInitial = data.userStatus.availablePoints
|
||||
|
||||
log(this.isMobile, 'MAIN-POINTS', `Current point count: ${this.pointsInitial}`)
|
||||
|
||||
const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
|
||||
|
||||
// Tally all the desktop points
|
||||
this.pointsCanCollect = browserEnarablePoints.dailySetPoints +
|
||||
browserEnarablePoints.desktopSearchPoints
|
||||
+ browserEnarablePoints.morePromotionsPoints
|
||||
|
||||
log(this.isMobile, 'MAIN-POINTS', `You can earn ${this.pointsCanCollect} points today`)
|
||||
|
||||
// If runOnZeroPoints is false and 0 points to earn, don't continue
|
||||
if (!this.config.runOnZeroPoints && this.pointsCanCollect === 0) {
|
||||
log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow')
|
||||
|
||||
// Close desktop browser
|
||||
await this.browser.func.closeBrowser(browser, account.email)
|
||||
return
|
||||
}
|
||||
|
||||
// Open a new tab to where the tasks are going to be completed
|
||||
const workerPage = await browser.newPage()
|
||||
|
||||
// Go to homepage on worker page
|
||||
await this.browser.func.goHome(workerPage)
|
||||
|
||||
// Complete daily set
|
||||
if (this.config.workers.doDailySet) {
|
||||
await this.workers.doDailySet(workerPage, data)
|
||||
}
|
||||
|
||||
// Complete more promotions
|
||||
if (this.config.workers.doMorePromotions) {
|
||||
await this.workers.doMorePromotions(workerPage, data)
|
||||
}
|
||||
|
||||
// Complete punch cards
|
||||
if (this.config.workers.doPunchCards) {
|
||||
await this.workers.doPunchCard(workerPage, data)
|
||||
}
|
||||
|
||||
// Do desktop searches
|
||||
if (this.config.workers.doDesktopSearch) {
|
||||
await this.activities.doSearch(workerPage, data)
|
||||
}
|
||||
|
||||
// Save cookies
|
||||
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
|
||||
|
||||
// Close desktop browser
|
||||
await this.browser.func.closeBrowser(browser, account.email)
|
||||
return
|
||||
}
|
||||
|
||||
// Mobile
|
||||
async Mobile(account: Account) {
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
log(this.isMobile, 'MAIN', 'Starting browser')
|
||||
|
||||
// Login into MS Rewards, then go to rewards homepage
|
||||
await this.login.login(this.homePage, account.email, account.password)
|
||||
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
|
||||
|
||||
await this.browser.func.goHome(this.homePage)
|
||||
|
||||
const data = await this.browser.func.getDashboardData()
|
||||
|
||||
const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
|
||||
const appEarnablePoints = await this.browser.func.getAppEarnablePoints(this.accessToken)
|
||||
|
||||
this.pointsCanCollect = browserEnarablePoints.mobileSearchPoints + appEarnablePoints.totalEarnablePoints
|
||||
|
||||
log(this.isMobile, 'MAIN-POINTS', `You can earn ${this.pointsCanCollect} points today (Browser: ${browserEnarablePoints.mobileSearchPoints} points, App: ${appEarnablePoints.totalEarnablePoints} points)`)
|
||||
|
||||
// If runOnZeroPoints is false and 0 points to earn, don't continue
|
||||
if (!this.config.runOnZeroPoints && this.pointsCanCollect === 0) {
|
||||
log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow')
|
||||
|
||||
// Close mobile browser
|
||||
await this.browser.func.closeBrowser(browser, account.email)
|
||||
return
|
||||
}
|
||||
|
||||
// Do daily check in
|
||||
if (this.config.workers.doDailyCheckIn) {
|
||||
await this.activities.doDailyCheckIn(this.accessToken, data)
|
||||
}
|
||||
|
||||
// Do read to earn
|
||||
if (this.config.workers.doReadToEarn) {
|
||||
await this.activities.doReadToEarn(this.accessToken, data)
|
||||
}
|
||||
|
||||
// Do mobile searches
|
||||
if (this.config.workers.doMobileSearch) {
|
||||
// If no mobile searches data found, stop (Does not always exist on new accounts)
|
||||
if (data.userStatus.counters.mobileSearch) {
|
||||
// Open a new tab to where the tasks are going to be completed
|
||||
const workerPage = await browser.newPage()
|
||||
|
||||
// Go to homepage on worker page
|
||||
await this.browser.func.goHome(workerPage)
|
||||
|
||||
await this.activities.doSearch(workerPage, data)
|
||||
|
||||
// Fetch current search points
|
||||
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
|
||||
|
||||
if (mobileSearchPoints && (mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0) {
|
||||
// Increment retry count
|
||||
this.mobileRetryAttempts++
|
||||
}
|
||||
|
||||
// Exit if retries are exhausted
|
||||
if (this.mobileRetryAttempts > this.config.searchSettings.retryMobileSearchAmount) {
|
||||
log(this.isMobile, 'MAIN', `Max retry limit of ${this.config.searchSettings.retryMobileSearchAmount} reached. Exiting retry loop`, 'warn')
|
||||
} else if (this.mobileRetryAttempts !== 0) {
|
||||
log(this.isMobile, 'MAIN', `Attempt ${this.mobileRetryAttempts}/${this.config.searchSettings.retryMobileSearchAmount}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow')
|
||||
|
||||
// Close mobile browser
|
||||
await this.browser.func.closeBrowser(browser, account.email)
|
||||
|
||||
// Create a new browser and try
|
||||
await this.Mobile(account)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log(this.isMobile, 'MAIN', 'Unable to fetch search points, your account is most likely too "new" for this! Try again later!', 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
const afterPointAmount = await this.browser.func.getCurrentPoints()
|
||||
|
||||
log(this.isMobile, 'MAIN-POINTS', `The script collected ${afterPointAmount - this.pointsInitial} points today`)
|
||||
|
||||
// Close mobile browser
|
||||
await this.browser.func.closeBrowser(browser, account.email)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rewardsBot = new MicrosoftRewardsBot(false)
|
||||
|
||||
try {
|
||||
await rewardsBot.initialize()
|
||||
await rewardsBot.run()
|
||||
} catch (error) {
|
||||
log(false, 'MAIN-ERROR', `Error running desktop bot: ${error}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Start the bots
|
||||
main().catch(error => {
|
||||
log('main', 'MAIN-ERROR', `Error running bots: ${error}`, 'error')
|
||||
process.exit(1)
|
||||
})
|
||||
13
src/interface/Account.ts
Normal file
13
src/interface/Account.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface Account {
|
||||
email: string;
|
||||
password: string;
|
||||
proxy: AccountProxy;
|
||||
}
|
||||
|
||||
export interface AccountProxy {
|
||||
proxyAxios: boolean;
|
||||
url: string;
|
||||
port: number;
|
||||
password: string;
|
||||
username: string;
|
||||
}
|
||||
226
src/interface/AppUserData.ts
Normal file
226
src/interface/AppUserData.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
export interface AppUserData {
|
||||
response: Response;
|
||||
correlationId: string;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
profile: Profile;
|
||||
balance: number;
|
||||
counters: null;
|
||||
promotions: Promotion[];
|
||||
catalog: null;
|
||||
goal_item: GoalItem;
|
||||
activities: null;
|
||||
cashback: null;
|
||||
orders: Order[];
|
||||
rebateProfile: null;
|
||||
rebatePayouts: null;
|
||||
giveProfile: GiveProfile;
|
||||
autoRedeemProfile: null;
|
||||
autoRedeemItem: null;
|
||||
thirdPartyProfile: null;
|
||||
notifications: null;
|
||||
waitlist: null;
|
||||
autoOpenFlyout: null;
|
||||
coupons: null;
|
||||
recommendedAffordableCatalog: null;
|
||||
}
|
||||
|
||||
export interface GiveProfile {
|
||||
give_user: string;
|
||||
give_organization: { [key: string]: GiveOrganization | null };
|
||||
first_give_optin: string;
|
||||
last_give_optout: string;
|
||||
give_lifetime_balance: string;
|
||||
give_lifetime_donation_balance: string;
|
||||
give_balance: string;
|
||||
form: null;
|
||||
}
|
||||
|
||||
export interface GiveOrganization {
|
||||
give_organization_donation_points: number;
|
||||
give_organization_donation_point_to_currency_ratio: number;
|
||||
give_organization_donation_currency: number;
|
||||
}
|
||||
|
||||
export interface GoalItem {
|
||||
name: string;
|
||||
provider: string;
|
||||
price: number;
|
||||
attributes: GoalItemAttributes;
|
||||
config: GoalItemConfig;
|
||||
}
|
||||
|
||||
export interface GoalItemAttributes {
|
||||
category: string;
|
||||
CategoryDescription: string;
|
||||
'desc.group_text': string;
|
||||
'desc.legal_text'?: string;
|
||||
'desc.sc_description': string;
|
||||
'desc.sc_title': string;
|
||||
display_order: string;
|
||||
ExtraLargeImage: string;
|
||||
group: string;
|
||||
group_image: string;
|
||||
group_sc_image: string;
|
||||
group_title: string;
|
||||
hidden?: string;
|
||||
large_image: string;
|
||||
large_sc_image: string;
|
||||
medium_image: string;
|
||||
MobileImage: string;
|
||||
original_price: string;
|
||||
Remarks?: string;
|
||||
ShortText?: string;
|
||||
showcase?: string;
|
||||
small_image: string;
|
||||
title: string;
|
||||
cimsid: string;
|
||||
user_defined_goal?: string;
|
||||
disable_bot_redemptions?: string;
|
||||
'desc.large_text'?: string;
|
||||
english_title?: string;
|
||||
etid?: string;
|
||||
sku?: string;
|
||||
coupon_discount?: string;
|
||||
}
|
||||
|
||||
export interface GoalItemConfig {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
isHidden: string;
|
||||
PointToCurrencyConversionRatio: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
t: Date;
|
||||
sku: string;
|
||||
item_snapshot: ItemSnapshot;
|
||||
p: number;
|
||||
s: S;
|
||||
a: A;
|
||||
child_redemption: null;
|
||||
third_party_partner: null;
|
||||
log: Log[];
|
||||
}
|
||||
|
||||
export interface A {
|
||||
form?: string;
|
||||
OrderId: string;
|
||||
CorrelationId: string;
|
||||
Channel: string;
|
||||
Language: string;
|
||||
Country: string;
|
||||
EvaluationId: string;
|
||||
provider?: string;
|
||||
referenceOrderID?: string;
|
||||
externalRefID?: string;
|
||||
denomination?: string;
|
||||
rewardName?: string;
|
||||
sendEmail?: string;
|
||||
status?: string;
|
||||
createdAt?: Date;
|
||||
bal_before_deduct?: string;
|
||||
bal_after_deduct?: string;
|
||||
}
|
||||
|
||||
export interface ItemSnapshot {
|
||||
name: string;
|
||||
provider: string;
|
||||
price: number;
|
||||
attributes: GoalItemAttributes;
|
||||
config: ItemSnapshotConfig;
|
||||
}
|
||||
|
||||
export interface ItemSnapshotConfig {
|
||||
amount: string;
|
||||
countryCode: string;
|
||||
currencyCode: string;
|
||||
sku: string;
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
time: Date;
|
||||
from: From;
|
||||
to: S;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export enum From {
|
||||
Created = 'Created',
|
||||
RiskApproved = 'RiskApproved',
|
||||
RiskReview = 'RiskReview'
|
||||
}
|
||||
|
||||
export enum S {
|
||||
Cancelled = 'Cancelled',
|
||||
RiskApproved = 'RiskApproved',
|
||||
RiskReview = 'RiskReview',
|
||||
Shipped = 'Shipped'
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
ruid: string;
|
||||
attributes: ProfileAttributes;
|
||||
offline_attributes: OfflineAttributes;
|
||||
}
|
||||
|
||||
export interface ProfileAttributes {
|
||||
publisher: string;
|
||||
publisher_upd: Date;
|
||||
creative: string;
|
||||
creative_upd: Date;
|
||||
program: string;
|
||||
program_upd: Date;
|
||||
country: string;
|
||||
country_upd: Date;
|
||||
referrerhash: string;
|
||||
referrerhash_upd: Date;
|
||||
optout_upd: Date;
|
||||
language: string;
|
||||
language_upd: Date;
|
||||
target: string;
|
||||
target_upd: Date;
|
||||
created: Date;
|
||||
created_upd: Date;
|
||||
epuid: string;
|
||||
epuid_upd: Date;
|
||||
goal: string;
|
||||
goal_upd: Date;
|
||||
waitlistattributes: string;
|
||||
waitlistattributes_upd: Date;
|
||||
serpbotscore_upd: Date;
|
||||
iscashbackeligible: string;
|
||||
cbedc: string;
|
||||
rlscpct_upd: Date;
|
||||
give_user: string;
|
||||
rebcpc_upd: Date;
|
||||
SerpBotScore_upd: Date;
|
||||
AdsBotScore_upd: Date;
|
||||
dbs_upd: Date;
|
||||
rbs: string;
|
||||
rbs_upd: Date;
|
||||
iris_segmentation: string;
|
||||
iris_segmentation_upd: Date;
|
||||
}
|
||||
|
||||
export interface OfflineAttributes {
|
||||
}
|
||||
|
||||
export interface Promotion {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: { [key: string]: string };
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export enum Tag {
|
||||
AllowTrialUser = 'allow_trial_user',
|
||||
ExcludeGivePcparent = 'exclude_give_pcparent',
|
||||
ExcludeGlobalConfig = 'exclude_global_config',
|
||||
ExcludeHidden = 'exclude_hidden',
|
||||
LOCString = 'locString',
|
||||
NonGlobalConfig = 'non_global_config'
|
||||
}
|
||||
56
src/interface/Config.ts
Normal file
56
src/interface/Config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface Config {
|
||||
baseURL: string;
|
||||
sessionPath: string;
|
||||
headless: boolean;
|
||||
parallel: boolean;
|
||||
runOnZeroPoints: boolean;
|
||||
clusters: number;
|
||||
saveFingerprint: ConfigSaveFingerprint;
|
||||
recoveryEmail?: string;
|
||||
workers: ConfigWorkers;
|
||||
searchOnBingLocalQueries: boolean;
|
||||
globalTimeout: number | string;
|
||||
searchSettings: ConfigSearchSettings;
|
||||
logExcludeFunc: string[];
|
||||
webhookLogExcludeFunc: string[];
|
||||
proxy: ConfigProxy;
|
||||
webhook: ConfigWebhook;
|
||||
}
|
||||
|
||||
export interface ConfigSaveFingerprint {
|
||||
mobile: boolean;
|
||||
desktop: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigSearchSettings {
|
||||
useGeoLocaleQueries: boolean;
|
||||
scrollRandomResults: boolean;
|
||||
clickRandomResults: boolean;
|
||||
searchDelay: ConfigSearchDelay;
|
||||
retryMobileSearchAmount: number;
|
||||
}
|
||||
|
||||
export interface ConfigSearchDelay {
|
||||
min: number | string;
|
||||
max: number | string;
|
||||
}
|
||||
|
||||
export interface ConfigWebhook {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ConfigProxy {
|
||||
proxyGoogleTrends: boolean;
|
||||
proxyBingTerms: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigWorkers {
|
||||
doDailySet: boolean;
|
||||
doMorePromotions: boolean;
|
||||
doPunchCards: boolean;
|
||||
doDesktopSearch: boolean;
|
||||
doMobileSearch: boolean;
|
||||
doDailyCheckIn: boolean;
|
||||
doReadToEarn: boolean;
|
||||
}
|
||||
701
src/interface/DashboardData.ts
Normal file
701
src/interface/DashboardData.ts
Normal file
@@ -0,0 +1,701 @@
|
||||
export interface DashboardData {
|
||||
userStatus: UserStatus;
|
||||
promotionalItem: PromotionalItem;
|
||||
dailySetPromotions: { [key: string]: PromotionalItem[] };
|
||||
streakPromotion: StreakPromotion;
|
||||
streakBonusPromotions: StreakBonusPromotion[];
|
||||
punchCards: PunchCard[];
|
||||
dashboardFlights: DashboardFlights;
|
||||
morePromotions: MorePromotion[];
|
||||
suggestedRewards: AutoRedeemItem[];
|
||||
coachMarks: CoachMarks;
|
||||
welcomeTour: WelcomeTour;
|
||||
userInterests: UserInterests;
|
||||
isVisualParityTest: boolean;
|
||||
mbingFlight: null;
|
||||
langCountryMismatchPromo: null;
|
||||
machineTranslationPromo: MachineTranslationPromo;
|
||||
autoRedeemItem: AutoRedeemItem;
|
||||
userProfile: UserProfile;
|
||||
}
|
||||
|
||||
export interface AutoRedeemItem {
|
||||
name: null | string;
|
||||
price: number;
|
||||
provider: null | string;
|
||||
disabled: boolean;
|
||||
category: string;
|
||||
title: string;
|
||||
variableGoalSpecificTitle: string;
|
||||
smallImageUrl: string;
|
||||
mediumImageUrl: string;
|
||||
largeImageUrl: string;
|
||||
largeShowcaseImageUrl: string;
|
||||
description: Description;
|
||||
showcase: boolean;
|
||||
showcaseInAllCategory: boolean;
|
||||
originalPrice: number;
|
||||
discountedPrice: number;
|
||||
popular: boolean;
|
||||
isTestOnly: boolean;
|
||||
groupId: string;
|
||||
inGroup: boolean;
|
||||
isDefaultItemInGroup: boolean;
|
||||
groupTitle: string;
|
||||
groupImageUrl: string;
|
||||
groupShowcaseImageUrl: string;
|
||||
instantWinGameId: string;
|
||||
instantWinPlayAgainSku: string;
|
||||
isLowInStock: boolean;
|
||||
isOutOfStock: boolean;
|
||||
getCodeMessage: string;
|
||||
disableEmail: boolean;
|
||||
stockMessage: string;
|
||||
comingSoonFlag: boolean;
|
||||
isGenericDonation: boolean;
|
||||
isVariableRedemptionItem: boolean;
|
||||
variableRedemptionItemCurrencySymbol: null;
|
||||
variableRedemptionItemMin: number;
|
||||
variableRedemptionItemMax: number;
|
||||
variableItemConfigPointsToCurrencyConversionRatio: number;
|
||||
isAutoRedeem: boolean;
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
itemGroupText: string;
|
||||
smallText: string;
|
||||
largeText: string;
|
||||
legalText: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
}
|
||||
|
||||
export interface CoachMarks {
|
||||
streaks: WelcomeTour;
|
||||
}
|
||||
|
||||
export interface WelcomeTour {
|
||||
promotion: DashboardImpression;
|
||||
slides: Slide[];
|
||||
}
|
||||
|
||||
export interface DashboardImpression {
|
||||
name: null | string;
|
||||
priority: number;
|
||||
attributes: { [key: string]: string } | null;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
benefits?: Benefit[];
|
||||
supportedLevelKeys?: string[];
|
||||
supportedLevelTitles?: string[];
|
||||
supportedLevelTitlesMobile?: string[];
|
||||
activeLevel?: string;
|
||||
isCodexAutoJoinUser?: boolean;
|
||||
}
|
||||
|
||||
export interface Benefit {
|
||||
key: string;
|
||||
text: string;
|
||||
url: null | string;
|
||||
helpText: null | string;
|
||||
supportedLevels: SupportedLevels;
|
||||
}
|
||||
|
||||
export interface SupportedLevels {
|
||||
level1?: string;
|
||||
level2: string;
|
||||
level2XBoxGold: string;
|
||||
}
|
||||
|
||||
export interface Slide {
|
||||
slideType: null;
|
||||
slideShowTourId: string;
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: null;
|
||||
subtitle1: null;
|
||||
description: string;
|
||||
description1: null;
|
||||
imageTitle: null;
|
||||
image2Title: null | string;
|
||||
image3Title: null | string;
|
||||
image4Title: null | string;
|
||||
imageDescription: null;
|
||||
image2Description: null | string;
|
||||
image3Description: null | string;
|
||||
image4Description: null | string;
|
||||
imageUrl: null | string;
|
||||
darkImageUrl: null;
|
||||
image2Url: null | string;
|
||||
image3Url: null | string;
|
||||
image4Url: null | string;
|
||||
layout: null | string;
|
||||
actionButtonText: null | string;
|
||||
actionButtonUrl: null | string;
|
||||
foregroundImageUrl: null;
|
||||
backLink: null;
|
||||
nextLink: CloseLink;
|
||||
closeLink: CloseLink;
|
||||
footnote: null | string;
|
||||
termsText: null;
|
||||
termsUrl: null;
|
||||
privacyText: null;
|
||||
privacyUrl: null;
|
||||
taggedItem: null | string;
|
||||
slideVisited: boolean;
|
||||
aboutPageLinkText: null;
|
||||
aboutPageLink: null;
|
||||
redeemLink: null;
|
||||
rewardsLink: null;
|
||||
quizLinks?: string[];
|
||||
quizCorrectAnswerTitle?: string;
|
||||
quizWrongAnswerTitle?: string;
|
||||
quizAnswerDescription?: string;
|
||||
}
|
||||
|
||||
export interface CloseLink {
|
||||
text: null | string;
|
||||
url: null | string;
|
||||
}
|
||||
|
||||
export interface PromotionalItem {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: PromotionalItemAttributes;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: Type;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
}
|
||||
|
||||
export interface PromotionalItemAttributes {
|
||||
animated_icon?: string;
|
||||
bg_image: string;
|
||||
complete: GiveEligible;
|
||||
daily_set_date?: string;
|
||||
description: string;
|
||||
destination: string;
|
||||
icon: string;
|
||||
image: string;
|
||||
link_text: string;
|
||||
max: string;
|
||||
offerid: string;
|
||||
progress: string;
|
||||
sc_bg_image: string;
|
||||
sc_bg_large_image: string;
|
||||
small_image: string;
|
||||
state: State;
|
||||
title: string;
|
||||
type: Type;
|
||||
give_eligible: GiveEligible;
|
||||
activity_max?: string;
|
||||
activity_progress?: string;
|
||||
is_wot?: GiveEligible;
|
||||
offer_counter?: string;
|
||||
promotional?: GiveEligible;
|
||||
parentPunchcards?: string;
|
||||
'classification.DescriptionText'?: string;
|
||||
'classification.PunchcardChildrenCount'?: string;
|
||||
'classification.PunchcardEndDate'?: Date;
|
||||
'classification.Template'?: string;
|
||||
'classification.TitleText'?: string;
|
||||
}
|
||||
|
||||
export enum GiveEligible {
|
||||
False = 'False',
|
||||
True = 'True'
|
||||
}
|
||||
|
||||
export enum State {
|
||||
Default = 'Default'
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
Quiz = 'quiz',
|
||||
Urlreward = 'urlreward',
|
||||
UrlrewardUrlrewardUrlrewardUrlrewardUrlreward = 'urlreward,urlreward,urlreward,urlreward,urlreward'
|
||||
}
|
||||
|
||||
export interface DashboardFlights {
|
||||
dashboardbannernav: string;
|
||||
togglegiveuser: string;
|
||||
spotifyRedirect: string;
|
||||
give_eligible: GiveEligible;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface MachineTranslationPromo {
|
||||
}
|
||||
|
||||
export interface MorePromotion {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: { [key: string]: string };
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
exclusiveLockedFeatureType: string;
|
||||
exclusiveLockedFeatureStatus: string;
|
||||
}
|
||||
|
||||
export interface PunchCard {
|
||||
name: string;
|
||||
parentPromotion?: PromotionalItem;
|
||||
childPromotions: PromotionalItem[];
|
||||
}
|
||||
|
||||
export interface StreakBonusPromotion {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: StreakBonusPromotionAttributes;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
}
|
||||
|
||||
export interface StreakBonusPromotionAttributes {
|
||||
hidden: GiveEligible;
|
||||
type: string;
|
||||
title: string;
|
||||
description: string;
|
||||
description_localizedkey: string;
|
||||
image: string;
|
||||
animated_icon: string;
|
||||
activity_progress: string;
|
||||
activity_max: string;
|
||||
bonus_earned: string;
|
||||
break_description: string;
|
||||
give_eligible: GiveEligible;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface StreakPromotion {
|
||||
lastUpdatedDate: Date;
|
||||
breakImageUrl: string;
|
||||
lifetimeMaxValue: number;
|
||||
bonusPointsEarned: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: StreakPromotionAttributes;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
}
|
||||
|
||||
export interface StreakPromotionAttributes {
|
||||
hidden: GiveEligible;
|
||||
type: string;
|
||||
title: string;
|
||||
image: string;
|
||||
activity_progress: string;
|
||||
last_updated: Date;
|
||||
break_image: string;
|
||||
lifetime_max: string;
|
||||
bonus_points: string;
|
||||
give_eligible: GiveEligible;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface UserInterests {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: UserInterestsAttributes;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
}
|
||||
|
||||
export interface UserInterestsAttributes {
|
||||
hidden: GiveEligible;
|
||||
give_eligible: GiveEligible;
|
||||
destination: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
ruid: string;
|
||||
attributes: UserProfileAttributes;
|
||||
}
|
||||
|
||||
export interface UserProfileAttributes {
|
||||
publisher: string;
|
||||
publisher_upd: Date;
|
||||
creative: string;
|
||||
creative_upd: Date;
|
||||
program: string;
|
||||
program_upd: Date;
|
||||
country: string;
|
||||
country_upd: Date;
|
||||
referrerhash: string;
|
||||
referrerhash_upd: Date;
|
||||
optout_upd: Date;
|
||||
language: string;
|
||||
language_upd: Date;
|
||||
target: string;
|
||||
target_upd: Date;
|
||||
created: Date;
|
||||
created_upd: Date;
|
||||
epuid: string;
|
||||
epuid_upd: Date;
|
||||
waitlistattributes: string;
|
||||
waitlistattributes_upd: Date;
|
||||
cbedc: GiveEligible;
|
||||
iscashbackeligible: GiveEligible;
|
||||
give_user: GiveEligible;
|
||||
}
|
||||
|
||||
export interface UserStatus {
|
||||
levelInfo: LevelInfo;
|
||||
availablePoints: number;
|
||||
lifetimePoints: number;
|
||||
lifetimePointsRedeemed: number;
|
||||
ePuid: string;
|
||||
redeemGoal: AutoRedeemItem;
|
||||
counters: Counters;
|
||||
lastOrder: LastOrder;
|
||||
dashboardImpression: DashboardImpression;
|
||||
referrerProgressInfo: ReferrerProgressInfo;
|
||||
isGiveModeOn: boolean;
|
||||
giveBalance: number;
|
||||
firstTimeGiveModeOptIn: null;
|
||||
giveOrganizationName: string;
|
||||
lifetimeGivingPoints: number;
|
||||
isRewardsUser: boolean;
|
||||
isMuidTrialUser: boolean;
|
||||
}
|
||||
|
||||
export interface Counters {
|
||||
pcSearch: DashboardImpression[];
|
||||
mobileSearch?: DashboardImpression[];
|
||||
shopAndEarn: DashboardImpression[];
|
||||
activityAndQuiz: ActivityAndQuiz[];
|
||||
dailyPoint: DashboardImpression[];
|
||||
}
|
||||
|
||||
export interface ActivityAndQuiz {
|
||||
name: string;
|
||||
priority: number;
|
||||
attributes: ActivityAndQuizAttributes;
|
||||
offerId: string;
|
||||
complete: boolean;
|
||||
counter: number;
|
||||
activityProgress: number;
|
||||
activityProgressMax: number;
|
||||
pointProgressMax: number;
|
||||
pointProgress: number;
|
||||
promotionType: string;
|
||||
promotionSubtype: string;
|
||||
title: string;
|
||||
extBannerTitle: string;
|
||||
titleStyle: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
extBannerDescription: string;
|
||||
descriptionStyle: string;
|
||||
showcaseTitle: string;
|
||||
showcaseDescription: string;
|
||||
imageUrl: string;
|
||||
dynamicImage: string;
|
||||
smallImageUrl: string;
|
||||
backgroundImageUrl: string;
|
||||
showcaseBackgroundImageUrl: string;
|
||||
showcaseBackgroundLargeImageUrl: string;
|
||||
promotionBackgroundLeft: string;
|
||||
promotionBackgroundRight: string;
|
||||
iconUrl: string;
|
||||
animatedIconUrl: string;
|
||||
animatedLargeBackgroundImageUrl: string;
|
||||
destinationUrl: string;
|
||||
linkText: string;
|
||||
hash: string;
|
||||
activityType: string;
|
||||
isRecurring: boolean;
|
||||
isHidden: boolean;
|
||||
isTestOnly: boolean;
|
||||
isGiveEligible: boolean;
|
||||
level: string;
|
||||
slidesCount: number;
|
||||
legalText: string;
|
||||
legalLinkText: string;
|
||||
deviceType: string;
|
||||
}
|
||||
|
||||
export interface ActivityAndQuizAttributes {
|
||||
type: string;
|
||||
title: string;
|
||||
link_text: string;
|
||||
description: string;
|
||||
foreground_color: string;
|
||||
image: string;
|
||||
recurring: string;
|
||||
destination: string;
|
||||
'classification.ShowProgress': GiveEligible;
|
||||
hidden: GiveEligible;
|
||||
give_eligible: GiveEligible;
|
||||
}
|
||||
|
||||
export interface LastOrder {
|
||||
id: null;
|
||||
price: number;
|
||||
status: null;
|
||||
sku: null;
|
||||
timestamp: Date;
|
||||
catalogItem: null;
|
||||
}
|
||||
|
||||
export interface LevelInfo {
|
||||
activeLevel: string;
|
||||
activeLevelName: string;
|
||||
progress: number;
|
||||
progressMax: number;
|
||||
levels: Level[];
|
||||
benefitsPromotion: DashboardImpression;
|
||||
}
|
||||
|
||||
export interface Level {
|
||||
key: string;
|
||||
active: boolean;
|
||||
name: string;
|
||||
tasks: CloseLink[];
|
||||
privileges: CloseLink[];
|
||||
}
|
||||
|
||||
export interface ReferrerProgressInfo {
|
||||
pointsEarned: number;
|
||||
pointsMax: number;
|
||||
isComplete: boolean;
|
||||
promotions: string[];
|
||||
}
|
||||
44
src/interface/GoogleDailyTrends.ts
Normal file
44
src/interface/GoogleDailyTrends.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface GoogleTrends {
|
||||
default: Default;
|
||||
}
|
||||
|
||||
export interface Default {
|
||||
trendingSearchesDays: TrendingSearchesDay[];
|
||||
endDateForNextRequest: string;
|
||||
rssFeedPageUrl: string;
|
||||
}
|
||||
|
||||
export interface TrendingSearchesDay {
|
||||
date: string;
|
||||
formattedDate: string;
|
||||
trendingSearches: TrendingSearch[];
|
||||
}
|
||||
|
||||
export interface TrendingSearch {
|
||||
title: Title;
|
||||
formattedTraffic: string;
|
||||
relatedQueries: Title[];
|
||||
image: Image;
|
||||
articles: Article[];
|
||||
shareUrl: string;
|
||||
}
|
||||
|
||||
export interface Article {
|
||||
title: string;
|
||||
timeAgo: string;
|
||||
source: string;
|
||||
image?: Image;
|
||||
url: string;
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
newsUrl: string;
|
||||
source: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export interface Title {
|
||||
query: string;
|
||||
exploreLink: string;
|
||||
}
|
||||
9
src/interface/OAuth.ts
Normal file
9
src/interface/OAuth.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface OAuth {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: number;
|
||||
foci: string;
|
||||
token_type: string;
|
||||
}
|
||||
7
src/interface/Points.ts
Normal file
7
src/interface/Points.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface EarnablePoints {
|
||||
desktopSearchPoints: number
|
||||
mobileSearchPoints: number
|
||||
dailySetPoints: number
|
||||
morePromotionsPoints: number
|
||||
totalEarnablePoints: number
|
||||
}
|
||||
50
src/interface/QuizData.ts
Normal file
50
src/interface/QuizData.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export interface QuizData {
|
||||
offerId: string;
|
||||
quizId: string;
|
||||
quizCategory: string;
|
||||
IsCurrentQuestionCompleted: boolean;
|
||||
quizRenderSummaryPage: boolean;
|
||||
resetQuiz: boolean;
|
||||
userClickedOnHint: boolean;
|
||||
isDemoEnabled: boolean;
|
||||
correctAnswer: string;
|
||||
isMultiChoiceQuizType: boolean;
|
||||
isPutInOrderQuizType: boolean;
|
||||
isListicleQuizType: boolean;
|
||||
isWOTQuizType: boolean;
|
||||
isBugsForRewardsQuizType: boolean;
|
||||
currentQuestionNumber: number;
|
||||
maxQuestions: number;
|
||||
resetTrackingCounters: boolean;
|
||||
showWelcomePanel: boolean;
|
||||
isAjaxCall: boolean;
|
||||
showHint: boolean;
|
||||
numberOfOptions: number;
|
||||
isMobile: boolean;
|
||||
inRewardsMode: boolean;
|
||||
enableDailySetWelcomePane: boolean;
|
||||
enableDailySetNonWelcomePane: boolean;
|
||||
isDailySetUrlOffer: boolean;
|
||||
isDailySetFlightEnabled: boolean;
|
||||
dailySetUrlOfferId: string;
|
||||
earnedCredits: number;
|
||||
maxCredits: number;
|
||||
creditsPerQuestion: number;
|
||||
userAlreadyClickedOptions: number;
|
||||
hasUserClickedOnOption: boolean;
|
||||
recentAnswerChoice: string;
|
||||
sessionTimerSeconds: string;
|
||||
isOverlayMinimized: number;
|
||||
ScreenReaderMsgOnMove: string;
|
||||
ScreenReaderMsgOnDrop: string;
|
||||
IsPartialPointsEnabled: boolean;
|
||||
PrioritizeUrlOverCookies: boolean;
|
||||
UseNewReportActivityAPI: boolean;
|
||||
CorrectlyAnsweredQuestionCount: number;
|
||||
showJoinRewardsPage: boolean;
|
||||
CorrectOptionAnswer_WOT: string;
|
||||
WrongOptionAnswer_WOT: string;
|
||||
enableSlideAnimation: boolean;
|
||||
ariaLoggingEnabled: boolean;
|
||||
UseQuestionIndexInActivityId: boolean;
|
||||
}
|
||||
4
src/interface/Search.ts
Normal file
4
src/interface/Search.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface GoogleSearch {
|
||||
topic: string;
|
||||
related: string[];
|
||||
}
|
||||
62
src/interface/UserAgentUtil.ts
Normal file
62
src/interface/UserAgentUtil.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Chrome Product Data
|
||||
export interface ChromeVersion {
|
||||
timestamp: Date;
|
||||
channels: Channels;
|
||||
}
|
||||
|
||||
export interface Channels {
|
||||
Stable: Beta;
|
||||
Beta: Beta;
|
||||
Dev: Beta;
|
||||
Canary: Beta;
|
||||
}
|
||||
|
||||
export interface Beta {
|
||||
channel: string;
|
||||
version: string;
|
||||
revision: string;
|
||||
}
|
||||
|
||||
// Edge Product Data
|
||||
export interface EdgeVersion {
|
||||
Product: string;
|
||||
Releases: Release[];
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
ReleaseId: number;
|
||||
Platform: Platform;
|
||||
Architecture: Architecture;
|
||||
CVEs: string[];
|
||||
ProductVersion: string;
|
||||
Artifacts: Artifact[];
|
||||
PublishedTime: Date;
|
||||
ExpectedExpiryDate: Date;
|
||||
}
|
||||
|
||||
export enum Architecture {
|
||||
Arm64 = 'arm64',
|
||||
Universal = 'universal',
|
||||
X64 = 'x64',
|
||||
X86 = 'x86'
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
ArtifactName: string;
|
||||
Location: string;
|
||||
Hash: string;
|
||||
HashAlgorithm: HashAlgorithm;
|
||||
SizeInBytes: number;
|
||||
}
|
||||
|
||||
export enum HashAlgorithm {
|
||||
Sha256 = 'SHA256'
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
Android = 'Android',
|
||||
IOS = 'iOS',
|
||||
Linux = 'Linux',
|
||||
MACOS = 'MacOS',
|
||||
Windows = 'Windows'
|
||||
}
|
||||
32
src/run_daily.sh
Normal file
32
src/run_daily.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set up environment variables
|
||||
export PATH=$PATH:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
|
||||
|
||||
# Ensure TZ is set
|
||||
export TZ=${TZ}
|
||||
|
||||
# Change directory to the application directory
|
||||
cd /usr/src/microsoft-rewards-script
|
||||
|
||||
# Define the minimum and maximum wait times in seconds
|
||||
MINWAIT=$((5*60)) # 5 minutes
|
||||
MAXWAIT=$((50*60)) # 50 minutes
|
||||
|
||||
# Calculate a random sleep time within the specified range
|
||||
SLEEPTIME=$((MINWAIT + RANDOM % (MAXWAIT - MINWAIT)))
|
||||
|
||||
# Convert the sleep time to minutes for logging
|
||||
SLEEP_MINUTES=$((SLEEPTIME / 60))
|
||||
|
||||
# Log the sleep duration
|
||||
echo "Sleeping for $SLEEP_MINUTES minutes ($SLEEPTIME seconds)..."
|
||||
|
||||
# Sleep for the calculated time
|
||||
sleep $SLEEPTIME
|
||||
|
||||
# Log the start of the script
|
||||
echo "Starting script..."
|
||||
|
||||
# Execute the Node.js script directly
|
||||
npm run start
|
||||
49
src/util/Axios.ts
Normal file
49
src/util/Axios.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
|
||||
class AxiosClient {
|
||||
private instance: AxiosInstance
|
||||
private account: AccountProxy
|
||||
|
||||
constructor(account: AccountProxy) {
|
||||
this.account = account
|
||||
this.instance = axios.create()
|
||||
|
||||
// If a proxy configuration is provided, set up the agent
|
||||
if (this.account.url && this.account.proxyAxios) {
|
||||
const agent = this.getAgentForProxy(this.account)
|
||||
this.instance.defaults.httpAgent = agent
|
||||
this.instance.defaults.httpsAgent = agent
|
||||
}
|
||||
}
|
||||
|
||||
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
|
||||
const { url, port } = proxyConfig
|
||||
|
||||
switch (true) {
|
||||
case proxyConfig.url.startsWith('http'):
|
||||
return new HttpProxyAgent(`${url}:${port}`)
|
||||
case proxyConfig.url.startsWith('https'):
|
||||
return new HttpsProxyAgent(`${url}:${port}`)
|
||||
case proxyConfig.url.startsWith('socks'):
|
||||
return new SocksProxyAgent(`${url}:${port}`)
|
||||
default:
|
||||
throw new Error(`Unsupported proxy protocol: ${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic method to make any Axios request
|
||||
public async request(config: AxiosRequestConfig, bypassProxy = false): Promise<AxiosResponse> {
|
||||
if (bypassProxy) {
|
||||
const bypassInstance = axios.create()
|
||||
return bypassInstance.request(config)
|
||||
}
|
||||
|
||||
return this.instance.request(config)
|
||||
}
|
||||
}
|
||||
|
||||
export default AxiosClient
|
||||
116
src/util/Load.ts
Normal file
116
src/util/Load.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { BrowserContext, Cookie } from 'rebrowser-playwright'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config, ConfigSaveFingerprint } from '../interface/Config'
|
||||
|
||||
let configCache: Config
|
||||
|
||||
export function loadAccounts(): Account[] {
|
||||
try {
|
||||
let file = 'accounts.json'
|
||||
|
||||
// If dev mode, use dev account(s)
|
||||
if (process.argv.includes('-dev')) {
|
||||
file = 'accounts.dev.json'
|
||||
}
|
||||
|
||||
const accountDir = path.join(__dirname, '../', file)
|
||||
const accounts = fs.readFileSync(accountDir, 'utf-8')
|
||||
|
||||
return JSON.parse(accounts)
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
}
|
||||
|
||||
export function loadConfig(): Config {
|
||||
try {
|
||||
if (configCache) {
|
||||
return configCache
|
||||
}
|
||||
|
||||
const configDir = path.join(__dirname, '../', 'config.json')
|
||||
const config = fs.readFileSync(configDir, 'utf-8')
|
||||
|
||||
const configData = JSON.parse(config)
|
||||
configCache = configData // Set as cache
|
||||
|
||||
return configData
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, saveFingerprint: ConfigSaveFingerprint) {
|
||||
try {
|
||||
// Fetch cookie file
|
||||
const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`)
|
||||
|
||||
let cookies: Cookie[] = []
|
||||
if (fs.existsSync(cookieFile)) {
|
||||
const cookiesData = await fs.promises.readFile(cookieFile, 'utf-8')
|
||||
cookies = JSON.parse(cookiesData)
|
||||
}
|
||||
|
||||
// Fetch fingerprint file
|
||||
const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
|
||||
|
||||
let fingerprint!: BrowserFingerprintWithHeaders
|
||||
if (((saveFingerprint.desktop && !isMobile) || (saveFingerprint.mobile && isMobile)) && fs.existsSync(fingerprintFile)) {
|
||||
const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8')
|
||||
fingerprint = JSON.parse(fingerprintData)
|
||||
}
|
||||
|
||||
return {
|
||||
cookies: cookies,
|
||||
fingerprint: fingerprint
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSessionData(sessionPath: string, browser: BrowserContext, email: string, isMobile: boolean): Promise<string> {
|
||||
try {
|
||||
const cookies = await browser.cookies()
|
||||
|
||||
// Fetch path
|
||||
const sessionDir = path.join(__dirname, '../browser/', sessionPath, email)
|
||||
|
||||
// Create session dir
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
await fs.promises.mkdir(sessionDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Save cookies to a file
|
||||
await fs.promises.writeFile(path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`), JSON.stringify(cookies))
|
||||
|
||||
return sessionDir
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveFingerprintData(sessionPath: string, email: string, isMobile: boolean, fingerpint: BrowserFingerprintWithHeaders): Promise<string> {
|
||||
try {
|
||||
// Fetch path
|
||||
const sessionDir = path.join(__dirname, '../browser/', sessionPath, email)
|
||||
|
||||
// Create session dir
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
await fs.promises.mkdir(sessionDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Save fingerprint to a file
|
||||
await fs.promises.writeFile(path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`), JSON.stringify(fingerpint))
|
||||
|
||||
return sessionDir
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
}
|
||||
45
src/util/Logger.ts
Normal file
45
src/util/Logger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { Webhook } from './Webhook'
|
||||
import { loadConfig } from './Load'
|
||||
|
||||
|
||||
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): void {
|
||||
const configData = loadConfig()
|
||||
|
||||
if (configData.logExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentTime = new Date().toLocaleString()
|
||||
const platformText = isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP'
|
||||
const chalkedPlatform = isMobile === 'main' ? chalk.bgCyan('MAIN') : isMobile ? chalk.bgBlue('MOBILE') : chalk.bgMagenta('DESKTOP')
|
||||
|
||||
// Clean string for the Webhook (no chalk)
|
||||
const cleanStr = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`
|
||||
|
||||
// Send the clean string to the Webhook
|
||||
if (!configData.webhookLogExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
|
||||
Webhook(configData, cleanStr)
|
||||
}
|
||||
|
||||
// Formatted string with chalk for terminal logging
|
||||
const str = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${chalkedPlatform} [${title}] ${message}`
|
||||
|
||||
const applyChalk = color && typeof chalk[color] === 'function' ? chalk[color] as (msg: string) => string : null
|
||||
|
||||
// Log based on the type
|
||||
switch (type) {
|
||||
case 'warn':
|
||||
applyChalk ? console.warn(applyChalk(str)) : console.warn(str)
|
||||
break
|
||||
|
||||
case 'error':
|
||||
applyChalk ? console.error(applyChalk(str)) : console.error(str)
|
||||
break
|
||||
|
||||
default:
|
||||
applyChalk ? console.log(applyChalk(str)) : console.log(str)
|
||||
break
|
||||
}
|
||||
}
|
||||
143
src/util/UserAgent.ts
Normal file
143
src/util/UserAgent.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import axios from 'axios'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
|
||||
import { log } from './Logger'
|
||||
|
||||
import { ChromeVersion, EdgeVersion } from '../interface/UserAgentUtil'
|
||||
|
||||
const NOT_A_BRAND_VERSION = '99'
|
||||
|
||||
export async function getUserAgent(isMobile: boolean) {
|
||||
const system = getSystemComponents(isMobile)
|
||||
const app = await getAppComponents(isMobile)
|
||||
|
||||
const uaTemplate = isMobile ?
|
||||
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Mobile Safari/537.36 EdgA/${app.edge_version}` :
|
||||
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Safari/537.36 Edg/${app.edge_version}`
|
||||
|
||||
const platformVersion = `${isMobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0`
|
||||
|
||||
const uaMetadata = {
|
||||
isMobile,
|
||||
platform: isMobile ? 'Android' : 'Windows',
|
||||
fullVersionList: [
|
||||
{ brand: 'Not/A)Brand', version: `${NOT_A_BRAND_VERSION}.0.0.0` },
|
||||
{ brand: 'Microsoft Edge', version: app['edge_version'] },
|
||||
{ brand: 'Chromium', version: app['chrome_version'] }
|
||||
],
|
||||
brands: [
|
||||
{ brand: 'Not/A)Brand', version: NOT_A_BRAND_VERSION },
|
||||
{ brand: 'Microsoft Edge', version: app['edge_major_version'] },
|
||||
{ brand: 'Chromium', version: app['chrome_major_version'] }
|
||||
],
|
||||
platformVersion,
|
||||
architecture: isMobile ? '' : 'x86',
|
||||
bitness: isMobile ? '' : '64',
|
||||
model: ''
|
||||
}
|
||||
|
||||
return { userAgent: uaTemplate, userAgentMetadata: uaMetadata }
|
||||
}
|
||||
|
||||
export async function getChromeVersion(isMobile: boolean): Promise<string> {
|
||||
try {
|
||||
const request = {
|
||||
url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(request)
|
||||
const data: ChromeVersion = response.data
|
||||
return data.channels.Stable.version
|
||||
|
||||
} catch (error) {
|
||||
throw log(isMobile, 'USERAGENT-CHROME-VERSION', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEdgeVersions(isMobile: boolean) {
|
||||
try {
|
||||
const request = {
|
||||
url: 'https://edgeupdates.microsoft.com/api/products',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(request)
|
||||
const data: EdgeVersion[] = response.data
|
||||
const stable = data.find(x => x.Product == 'Stable') as EdgeVersion
|
||||
return {
|
||||
android: stable.Releases.find(x => x.Platform == 'Android')?.ProductVersion,
|
||||
windows: stable.Releases.find(x => (x.Platform == 'Windows' && x.Architecture == 'x64'))?.ProductVersion
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw log(isMobile, 'USERAGENT-EDGE-VERSION', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
export function getSystemComponents(mobile: boolean): string {
|
||||
const osId: string = mobile ? 'Linux' : 'Windows NT 10.0'
|
||||
const uaPlatform: string = mobile ? `Android 1${Math.floor(Math.random() * 5)}` : 'Win64; x64'
|
||||
|
||||
if (mobile) {
|
||||
return `${uaPlatform}; ${osId}; K`
|
||||
}
|
||||
|
||||
return `${uaPlatform}; ${osId}`
|
||||
}
|
||||
|
||||
export async function getAppComponents(isMobile: boolean) {
|
||||
const versions = await getEdgeVersions(isMobile)
|
||||
const edgeVersion = isMobile ? versions.android : versions.windows as string
|
||||
const edgeMajorVersion = edgeVersion?.split('.')[0]
|
||||
|
||||
const chromeVersion = await getChromeVersion(isMobile)
|
||||
const chromeMajorVersion = chromeVersion?.split('.')[0]
|
||||
const chromeReducedVersion = `${chromeMajorVersion}.0.0.0`
|
||||
|
||||
return {
|
||||
not_a_brand_version: `${NOT_A_BRAND_VERSION}.0.0.0`,
|
||||
not_a_brand_major_version: NOT_A_BRAND_VERSION,
|
||||
edge_version: edgeVersion as string,
|
||||
edge_major_version: edgeMajorVersion as string,
|
||||
chrome_version: chromeVersion as string,
|
||||
chrome_major_version: chromeMajorVersion as string,
|
||||
chrome_reduced_version: chromeReducedVersion as string
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateFingerprintUserAgent(fingerprint: BrowserFingerprintWithHeaders, isMobile: boolean): Promise<BrowserFingerprintWithHeaders> {
|
||||
try {
|
||||
const userAgentData = await getUserAgent(isMobile)
|
||||
const componentData = await getAppComponents(isMobile)
|
||||
|
||||
//@ts-expect-error Errors due it not exactly matching
|
||||
fingerprint.fingerprint.navigator.userAgentData = userAgentData.userAgentMetadata
|
||||
fingerprint.fingerprint.navigator.userAgent = userAgentData.userAgent
|
||||
fingerprint.fingerprint.navigator.appVersion = userAgentData.userAgent.replace(`${fingerprint.fingerprint.navigator.appCodeName}/`, '')
|
||||
|
||||
fingerprint.headers['user-agent'] = userAgentData.userAgent
|
||||
fingerprint.headers['sec-ch-ua'] = `"Microsoft Edge";v="${componentData.edge_major_version}", "Not=A?Brand";v="${componentData.not_a_brand_major_version}", "Chromium";v="${componentData.chrome_major_version}"`
|
||||
fingerprint.headers['sec-ch-ua-full-version-list'] = `"Microsoft Edge";v="${componentData.edge_version}", "Not=A?Brand";v="${componentData.not_a_brand_version}", "Chromium";v="${componentData.chrome_version}"`
|
||||
|
||||
/*
|
||||
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36 EdgA/129.0.0.0
|
||||
sec-ch-ua-full-version-list: "Microsoft Edge";v="129.0.2792.84", "Not=A?Brand";v="8.0.0.0", "Chromium";v="129.0.6668.90"
|
||||
sec-ch-ua: "Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
|
||||
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
|
||||
"Google Chrome";v="129.0.6668.90", "Not=A?Brand";v="8.0.0.0", "Chromium";v="129.0.6668.90"
|
||||
*/
|
||||
|
||||
return fingerprint
|
||||
} catch (error) {
|
||||
throw log(isMobile, 'USER-AGENT-UPDATE', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
50
src/util/Utils.ts
Normal file
50
src/util/Utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import ms from 'ms'
|
||||
|
||||
export default class Util {
|
||||
|
||||
async wait(ms: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
getFormattedDate(ms = Date.now()): string {
|
||||
const today = new Date(ms)
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
const year = today.getFullYear()
|
||||
|
||||
return `${month}/${day}/${year}`
|
||||
}
|
||||
|
||||
shuffleArray<T>(array: T[]): T[] {
|
||||
return array.map(value => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value)
|
||||
}
|
||||
|
||||
randomNumber(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
||||
const chunkSize = Math.ceil(arr.length / numChunks)
|
||||
const chunks: T[][] = []
|
||||
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
const chunk = arr.slice(i, i + chunkSize)
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
stringToMs(input: string | number): number {
|
||||
const milisec = ms(input.toString())
|
||||
if (!milisec) {
|
||||
throw new Error('The string provided cannot be parsed to a valid time! Use a format like "1 min", "1m" or "1 minutes"')
|
||||
}
|
||||
return milisec
|
||||
}
|
||||
|
||||
}
|
||||
23
src/util/Webhook.ts
Normal file
23
src/util/Webhook.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'axios'
|
||||
|
||||
|
||||
import { Config } from '../interface/Config'
|
||||
|
||||
export async function Webhook(configData: Config, content: string) {
|
||||
const webhook = configData.webhook
|
||||
|
||||
if (!webhook.enabled || webhook.url.length < 10) return
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: webhook.url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
'content': content
|
||||
}
|
||||
}
|
||||
|
||||
await axios(request).catch(() => { })
|
||||
}
|
||||
74
tsconfig.json
Normal file
74
tsconfig.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
/* Basic Options */
|
||||
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": true, /* Enable strict null checks. */
|
||||
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||
// "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). */
|
||||
// "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. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/accounts.json",
|
||||
"src/config.json",
|
||||
"src/functions/queries.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user