mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-11 02:46:17 +00:00
Cleanup
This commit is contained in:
40
.eslintrc.js
40
.eslintrc.js
@@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,9 +1,11 @@
|
|||||||
sessions/
|
sessions/
|
||||||
dist/
|
dist/
|
||||||
|
.dev/
|
||||||
node_modules/
|
node_modules/
|
||||||
accounts.json
|
src/accounts.json
|
||||||
notes
|
src/config.json
|
||||||
|
note
|
||||||
accounts.dev.json
|
accounts.dev.json
|
||||||
accounts.main.json
|
accounts.main.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.playwright-chromium-installed
|
.playwright-chromium-installed
|
||||||
89
Dockerfile
89
Dockerfile
@@ -1,89 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# Stage 1: Builder
|
|
||||||
###############################################################################
|
|
||||||
FROM node:22-slim AS builder
|
|
||||||
|
|
||||||
WORKDIR /usr/src/microsoft-rewards-script
|
|
||||||
|
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package.json package-lock.json tsconfig.json ./
|
|
||||||
|
|
||||||
# Install all dependencies required to build the script
|
|
||||||
RUN npm ci --ignore-scripts
|
|
||||||
|
|
||||||
# Copy source and build
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Remove build dependencies, and reinstall only runtime dependencies
|
|
||||||
RUN rm -rf node_modules \
|
|
||||||
&& npm ci --omit=dev --ignore-scripts \
|
|
||||||
&& npm cache clean --force
|
|
||||||
|
|
||||||
# Install Chromium Headless Shell, and cleanup
|
|
||||||
RUN npx playwright install --with-deps --only-shell chromium \
|
|
||||||
&& rm -rf /root/.cache /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Stage 2: Runtime
|
|
||||||
###############################################################################
|
|
||||||
FROM node:22-slim AS runtime
|
|
||||||
|
|
||||||
WORKDIR /usr/src/microsoft-rewards-script
|
|
||||||
|
|
||||||
# Set production environment variables
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
TZ=UTC \
|
|
||||||
PLAYWRIGHT_BROWSERS_PATH=0 \
|
|
||||||
FORCE_HEADLESS=1
|
|
||||||
|
|
||||||
# Install minimal system libraries required for Chromium headless to run
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
cron \
|
|
||||||
gettext-base \
|
|
||||||
tzdata \
|
|
||||||
ca-certificates \
|
|
||||||
libglib2.0-0 \
|
|
||||||
libdbus-1-3 \
|
|
||||||
libexpat1 \
|
|
||||||
libfontconfig1 \
|
|
||||||
libgtk-3-0 \
|
|
||||||
libnspr4 \
|
|
||||||
libnss3 \
|
|
||||||
libasound2 \
|
|
||||||
libflac12 \
|
|
||||||
libatk1.0-0 \
|
|
||||||
libatspi2.0-0 \
|
|
||||||
libdrm2 \
|
|
||||||
libgbm1 \
|
|
||||||
libdav1d6 \
|
|
||||||
libx11-6 \
|
|
||||||
libx11-xcb1 \
|
|
||||||
libxcomposite1 \
|
|
||||||
libxcursor1 \
|
|
||||||
libxdamage1 \
|
|
||||||
libxext6 \
|
|
||||||
libxfixes3 \
|
|
||||||
libxi6 \
|
|
||||||
libxrandr2 \
|
|
||||||
libxrender1 \
|
|
||||||
libxss1 \
|
|
||||||
libxtst6 \
|
|
||||||
libdouble-conversion3 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
||||||
|
|
||||||
# Copy compiled application and dependencies from builder stage
|
|
||||||
COPY --from=builder /usr/src/microsoft-rewards-script/dist ./dist
|
|
||||||
COPY --from=builder /usr/src/microsoft-rewards-script/package*.json ./
|
|
||||||
COPY --from=builder /usr/src/microsoft-rewards-script/node_modules ./node_modules
|
|
||||||
|
|
||||||
# Copy runtime scripts with proper permissions from the start
|
|
||||||
COPY --chmod=755 src/run_daily.sh ./src/run_daily.sh
|
|
||||||
COPY --chmod=644 src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
|
|
||||||
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
||||||
|
|
||||||
# Entrypoint handles TZ, initial run toggle, cron templating & launch
|
|
||||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
||||||
CMD ["sh", "-c", "echo 'Container started; cron is running.'"]
|
|
||||||
292
README.md
292
README.md
@@ -1,297 +1,7 @@
|
|||||||
[](https://discord.gg/8BxYbV4pkj)
|
[](https://discord.gg/8BxYbV4pkj)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
TODO
|
||||||
## Table of Contents
|
|
||||||
- [Setup](#setup)
|
|
||||||
- [1. Clone the Repository](#1-clone-the-repository)
|
|
||||||
- [2. Copy Configuration Files](#2-copy-configuration-files)
|
|
||||||
- [3. Install Dependencies and Prepare the Browser](#3-install-dependencies-and-prepare-the-browser)
|
|
||||||
- [4. Build and Run](#4-build-and-run)
|
|
||||||
- [Nix Users](#nix-setup)
|
|
||||||
- [Docker Setup](#docker-setup)
|
|
||||||
- [Before Starting](#before-starting)
|
|
||||||
- [Quick Start](#quick-start)
|
|
||||||
- [Example compose.yaml](#example-composeyaml)
|
|
||||||
- [Configuration Reference](#configuration-reference)
|
|
||||||
- [Account Configuration](#account-configuration)
|
|
||||||
- [Features Overview](#features-overview)
|
|
||||||
- [Disclaimer](#disclaimer)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
**Requirements:** Node.js ≥ 20 and Git
|
|
||||||
Works on Windows, Linux, macOS, and WSL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1. Clone the Repository
|
|
||||||
**All systems:**
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/TheNetsky/Microsoft-Rewards-Script.git
|
|
||||||
cd Microsoft-Rewards-Script
|
|
||||||
```
|
|
||||||
Or download the latest release ZIP and extract it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Copy Configuration Files
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
Rename manually:
|
|
||||||
```
|
|
||||||
src/accounts.example.json → src/accounts.json
|
|
||||||
```
|
|
||||||
|
|
||||||
**Linux / macOS / WSL:**
|
|
||||||
```bash
|
|
||||||
cp src/accounts.example.json src/accounts.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Then edit:
|
|
||||||
- `src/accounts.json` — fill in your Microsoft account credentials.
|
|
||||||
- `src/config.json` — review or customize options.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Install Dependencies and Prepare the Browser
|
|
||||||
|
|
||||||
**All systems:**
|
|
||||||
```bash
|
|
||||||
npm run pre-build
|
|
||||||
```
|
|
||||||
|
|
||||||
This command:
|
|
||||||
- Installs all dependencies
|
|
||||||
- Clears old builds (`dist/`)
|
|
||||||
- Installs Playwright Chromium (required browser)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Build and Run
|
|
||||||
|
|
||||||
**All systems:**
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nix Setup
|
|
||||||
|
|
||||||
If using Nix:
|
|
||||||
|
|
||||||
1. Run the pre-build step first:
|
|
||||||
```bash
|
|
||||||
npm run pre-build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Then start the script:
|
|
||||||
```bash
|
|
||||||
./run.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will launch the script headlessly using `xvfb-run`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker Setup
|
|
||||||
|
|
||||||
### Before Starting
|
|
||||||
- Remove local `/node_modules` and `/dist` if previously built.
|
|
||||||
- Remove old Docker volumes if upgrading from older versions.
|
|
||||||
- You can reuse your existing `accounts.json`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
1. Clone the repository and configure your `accounts.json`.
|
|
||||||
2. Ensure `config.json` has `"headless": true`.
|
|
||||||
3. Edit `compose.yaml`:
|
|
||||||
- Set your timezone (`TZ`)
|
|
||||||
- Set the cron schedule (`CRON_SCHEDULE`)
|
|
||||||
- Optionally enable `RUN_ON_START=true`
|
|
||||||
4. Start the container:
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
5. Monitor logs:
|
|
||||||
```bash
|
|
||||||
docker logs microsoft-rewards-script
|
|
||||||
```
|
|
||||||
|
|
||||||
The container includes a randomized delay (about 5–50 minutes by default)
|
|
||||||
before each scheduled run to appear more natural. This can be configured or disabled via environment variables.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Example compose.yaml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
microsoft-rewards-script:
|
|
||||||
image: ghcr.io/your-org/microsoft-rewards-script:latest
|
|
||||||
container_name: microsoft-rewards-script
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
|
||||||
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
|
||||||
- ./sessions:/usr/src/microsoft-rewards-script/dist/sessions
|
|
||||||
|
|
||||||
environment:
|
|
||||||
TZ: "Europe/Amsterdam"
|
|
||||||
NODE_ENV: "production"
|
|
||||||
CRON_SCHEDULE: "0 7,16,20 * * *"
|
|
||||||
RUN_ON_START: "true"
|
|
||||||
# MIN_SLEEP_MINUTES: "5"
|
|
||||||
# MAX_SLEEP_MINUTES: "50"
|
|
||||||
# SKIP_RANDOM: "true"
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1.0"
|
|
||||||
memory: "1g"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### compose.yaml Notes
|
|
||||||
- **volumes**
|
|
||||||
- `accounts.json` and `config.json` are mounted read-only to prevent accidental edits.
|
|
||||||
- `sessions` persists login sessions and fingerprints across runs.
|
|
||||||
- If `jobState.enabled` is used, mount its directory as a volume.
|
|
||||||
- **CRON_SCHEDULE**
|
|
||||||
- Uses standard crontab syntax (e.g., via [crontab.guru](https://crontab.guru/)).
|
|
||||||
- Schedule is evaluated inside the container using the configured `TZ`.
|
|
||||||
- **RUN_ON_START**
|
|
||||||
- Runs the script once immediately on startup, then continues on schedule.
|
|
||||||
- **Randomization**
|
|
||||||
- Default delay: 5–50 minutes.
|
|
||||||
- Adjustable via `MIN_SLEEP_MINUTES` and `MAX_SLEEP_MINUTES`, or disable with `SKIP_RANDOM`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration Reference
|
|
||||||
|
|
||||||
Edit `src/config.json` to customize behavior.
|
|
||||||
Below is a summary of available options (matches the latest version in the repository).
|
|
||||||
|
|
||||||
### Core
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `baseURL` | string | `"https://rewards.bing.com"` | Microsoft Rewards base URL |
|
|
||||||
| `sessionPath` | string | `"sessions"` | Directory to store browser sessions |
|
|
||||||
| `headless` | boolean | `false` | Run browser invisibly |
|
|
||||||
| `parallel` | boolean | `false` | Run desktop and mobile simultaneously |
|
|
||||||
| `runOnZeroPoints` | boolean | `false` | Run even when no points are available |
|
|
||||||
| `clusters` | number | `1` | Number of concurrent account clusters |
|
|
||||||
| `globalTimeout` | string | `"30s"` | Timeout for all actions |
|
|
||||||
| `searchOnBingLocalQueries` | boolean | `false` | Use local query list |
|
|
||||||
|
|
||||||
### Fingerprinting
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `saveFingerprint.mobile` | boolean | `false` | Reuse mobile fingerprint |
|
|
||||||
| `saveFingerprint.desktop` | boolean | `false` | Reuse desktop fingerprint |
|
|
||||||
|
|
||||||
### Workers
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `doDailySet` | boolean | `true` | Complete daily set |
|
|
||||||
| `doMorePromotions` | boolean | `true` | Complete more promotions |
|
|
||||||
| `doPunchCards` | boolean | `true` | Complete punchcards |
|
|
||||||
| `doDesktopSearch` | boolean | `true` | Perform desktop searches |
|
|
||||||
| `doMobileSearch` | boolean | `true` | Perform mobile searches |
|
|
||||||
| `doDailyCheckIn` | boolean | `true` | Complete daily check-in |
|
|
||||||
| `doReadToEarn` | boolean | `true` | Complete Read-to-Earn |
|
|
||||||
|
|
||||||
### Search
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `searchSettings.useGeoLocaleQueries` | boolean | `false` | Use region-based queries |
|
|
||||||
| `searchSettings.scrollRandomResults` | boolean | `true` | Scroll randomly on results |
|
|
||||||
| `searchSettings.clickRandomResults` | boolean | `true` | Click random links |
|
|
||||||
| `searchSettings.searchDelay.min` | string | `"3min"` | Minimum delay between searches |
|
|
||||||
| `searchSettings.searchDelay.max` | string | `"5min"` | Maximum delay between searches |
|
|
||||||
| `searchSettings.retryMobileSearchAmount` | number | `2` | Retry mobile searches amount |
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `logExcludeFunc` | string[] | `["SEARCH-CLOSE-TABS"]` | Exclude from console logs |
|
|
||||||
| `webhookLogExcludeFunc` | string[] | `["SEARCH-CLOSE-TABS"]` | Exclude from webhook logs |
|
|
||||||
|
|
||||||
### Proxy
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `proxy.proxyGoogleTrends` | boolean | `true` | Proxy Google Trends requests |
|
|
||||||
| `proxy.proxyBingTerms` | boolean | `true` | Proxy Bing term requests |
|
|
||||||
|
|
||||||
### Webhooks
|
|
||||||
| Setting | Type | Default | Description |
|
|
||||||
|----------|------|----------|-------------|
|
|
||||||
| `webhook.enabled` | boolean | `false` | Enable Discord webhook |
|
|
||||||
| `webhook.url` | string | `""` | Webhook URL |
|
|
||||||
| `conclusionWebhook.enabled` | boolean | `false` | Enable summary webhook |
|
|
||||||
| `conclusionWebhook.url` | string | `""` | Summary webhook URL |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Account Configuration
|
|
||||||
|
|
||||||
Edit `src/accounts.json` — the file is an **array** of accounts:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notes**
|
|
||||||
- The file is a **flat array** — not `{ "accounts": [ ... ] }`.
|
|
||||||
- Only `email`, `password`, and `proxy` are supported.
|
|
||||||
- `proxyAxios` enables Axios-level proxying for API requests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features Overview
|
|
||||||
|
|
||||||
- Multi-account and session handling
|
|
||||||
- Persistent browser fingerprints
|
|
||||||
- Parallel task execution
|
|
||||||
- Proxy and retry support
|
|
||||||
- Human-like behavior simulation
|
|
||||||
- Full daily set automation
|
|
||||||
- Mobile and desktop search support
|
|
||||||
- Vacation and risk protection
|
|
||||||
- Webhook notifications
|
|
||||||
- Docker scheduling support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
42
compose.yaml
42
compose.yaml
@@ -1,42 +0,0 @@
|
|||||||
services:
|
|
||||||
microsoft-rewards-script:
|
|
||||||
build: .
|
|
||||||
container_name: microsoft-rewards-script
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
|
||||||
volumes:
|
|
||||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
|
||||||
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
|
||||||
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
|
|
||||||
|
|
||||||
environment:
|
|
||||||
TZ: "America/Toronto" # Set your timezone for proper scheduling
|
|
||||||
NODE_ENV: "production"
|
|
||||||
CRON_SCHEDULE: "0 7,16,20 * * *" # Customize your schedule, use crontab.guru for formatting
|
|
||||||
RUN_ON_START: "true" # Runs the script immediately on container startup
|
|
||||||
|
|
||||||
# Add scheduled start-time randomization (uncomment to customize or disable, default: enabled)
|
|
||||||
#MIN_SLEEP_MINUTES: "5"
|
|
||||||
#MAX_SLEEP_MINUTES: "50"
|
|
||||||
SKIP_RANDOM_SLEEP: "false"
|
|
||||||
|
|
||||||
# Optionally set how long to wait before killing a stuck script run (prevents blocking future runs, default: 8 hours)
|
|
||||||
#STUCK_PROCESS_TIMEOUT_HOURS: "8"
|
|
||||||
|
|
||||||
# Optional resource limits for the container
|
|
||||||
mem_limit: 4g
|
|
||||||
cpus: 2
|
|
||||||
|
|
||||||
# Health check - monitors if cron daemon is running to ensure scheduled jobs can execute
|
|
||||||
# Container marked unhealthy if cron process dies
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "sh", "-c", "pgrep cron > /dev/null || exit 1"]
|
|
||||||
interval: 60s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Ensure Playwright uses preinstalled browsers
|
|
||||||
export PLAYWRIGHT_BROWSERS_PATH=0
|
|
||||||
|
|
||||||
# 1. Timezone: default to UTC if not provided
|
|
||||||
: "${TZ:=UTC}"
|
|
||||||
ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
|
|
||||||
echo "$TZ" > /etc/timezone
|
|
||||||
dpkg-reconfigure -f noninteractive tzdata
|
|
||||||
|
|
||||||
# 2. Validate CRON_SCHEDULE
|
|
||||||
if [ -z "${CRON_SCHEDULE:-}" ]; then
|
|
||||||
echo "ERROR: CRON_SCHEDULE environment variable is not set." >&2
|
|
||||||
echo "Please set CRON_SCHEDULE (e.g., \"0 2 * * *\")." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Initial run without sleep if RUN_ON_START=true
|
|
||||||
if [ "${RUN_ON_START:-false}" = "true" ]; then
|
|
||||||
echo "[entrypoint] Starting initial run in background at $(date)"
|
|
||||||
(
|
|
||||||
cd /usr/src/microsoft-rewards-script || {
|
|
||||||
echo "[entrypoint-bg] ERROR: Unable to cd to /usr/src/microsoft-rewards-script" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
# Skip random sleep for initial run, but preserve setting for cron jobs
|
|
||||||
SKIP_RANDOM_SLEEP=true src/run_daily.sh
|
|
||||||
echo "[entrypoint-bg] Initial run completed at $(date)"
|
|
||||||
) &
|
|
||||||
echo "[entrypoint] Background process started (PID: $!)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. Template and register cron file with explicit timezone export
|
|
||||||
if [ ! -f /etc/cron.d/microsoft-rewards-cron.template ]; then
|
|
||||||
echo "ERROR: Cron template /etc/cron.d/microsoft-rewards-cron.template not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export TZ for envsubst to use
|
|
||||||
export TZ
|
|
||||||
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
|
|
||||||
|
|
||||||
echo "[entrypoint] Cron configured with schedule: $CRON_SCHEDULE and timezone: $TZ; starting cron at $(date)"
|
|
||||||
|
|
||||||
# 5. Start cron in foreground (PID 1)
|
|
||||||
exec cron -f
|
|
||||||
61
flake.lock
generated
61
flake.lock
generated
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1749727998,
|
|
||||||
"narHash": "sha256-mHv/yeUbmL91/TvV95p+mBVahm9mdQMJoqaTVTALaFw=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "fd487183437963a59ba763c0cc4f27e3447dd6dd",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
40
flake.nix
40
flake.nix
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
|
||||||
flake-utils = {
|
|
||||||
url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{ nixpkgs, flake-utils, ... }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
nodejs
|
|
||||||
playwright-driver.browsers
|
|
||||||
typescript
|
|
||||||
playwright-test
|
|
||||||
|
|
||||||
# fixes "waiting until load" issue compared to
|
|
||||||
# setting headless in config.json
|
|
||||||
xvfb-run
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
|
|
||||||
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
|
|
||||||
npm i
|
|
||||||
npm run build
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
3126
package-lock.json
generated
3126
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "microsoft-rewards-script",
|
|
||||||
"version": "1.5.3",
|
|
||||||
"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/node": "^20.14.11",
|
|
||||||
"@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.52.0",
|
|
||||||
"rebrowser-playwright": "1.52.0",
|
|
||||||
"socks-proxy-agent": "^8.0.5",
|
|
||||||
"ts-node": "^10.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
run.sh
3
run.sh
@@ -1,3 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
nix develop --command bash -c "xvfb-run npm run start"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import playwright, { BrowserContext } from 'rebrowser-playwright'
|
|
||||||
|
|
||||||
import { newInjectedContext } from 'fingerprint-injector'
|
|
||||||
import { FingerprintGenerator } from 'fingerprint-generator'
|
|
||||||
|
|
||||||
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> {
|
|
||||||
// Optional automatic browser installation (set AUTO_INSTALL_BROWSERS=1)
|
|
||||||
if (process.env.AUTO_INSTALL_BROWSERS === '1') {
|
|
||||||
try {
|
|
||||||
// Dynamically import child_process to avoid overhead otherwise
|
|
||||||
const { execSync } = await import('child_process') as any
|
|
||||||
execSync('npx playwright install chromium', { stdio: 'ignore' })
|
|
||||||
} catch { /* silent */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
let browser: any
|
|
||||||
try {
|
|
||||||
browser = await playwright.chromium.launch({
|
|
||||||
//channel: 'msedge', // Uses Edge instead of chrome
|
|
||||||
headless: this.bot.config.headless,
|
|
||||||
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--mute-audio',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--ignore-certificate-errors',
|
|
||||||
'--ignore-certificate-errors-spki-list',
|
|
||||||
'--ignore-ssl-errors'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = (e instanceof Error ? e.message : String(e))
|
|
||||||
// Common missing browser executable guidance
|
|
||||||
if (/Executable doesn't exist/i.test(msg)) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run: "npx playwright install chromium" (or set AUTO_INSTALL_BROWSERS=1 to auto attempt).', 'error')
|
|
||||||
} else {
|
|
||||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
|
|
||||||
|
|
||||||
const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint()
|
|
||||||
|
|
||||||
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
|
|
||||||
|
|
||||||
// Set timeout to preferred amount
|
|
||||||
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
|
|
||||||
|
|
||||||
await context.addCookies(sessionData.cookies)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
import { BrowserContext, Page } from 'rebrowser-playwright'
|
|
||||||
import { CheerioAPI, load } from 'cheerio'
|
|
||||||
import { AxiosRequestConfig } from 'axios'
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
let lastError: any = null
|
|
||||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
||||||
try {
|
|
||||||
// Reload the page to get new data
|
|
||||||
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
|
|
||||||
lastError = null
|
|
||||||
break
|
|
||||||
} catch (re) {
|
|
||||||
lastError = re
|
|
||||||
const msg = (re instanceof Error ? re.message : String(re))
|
|
||||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload failed attempt ${attempt}: ${msg}`, 'warn')
|
|
||||||
// If page/context closed => bail early after first retry
|
|
||||||
if (msg.includes('has been closed')) {
|
|
||||||
if (attempt === 1) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
|
||||||
try {
|
|
||||||
await this.goHome(this.bot.homePage)
|
|
||||||
} catch {/* ignore */}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (attempt === 2 && lastError) throw lastError
|
|
||||||
await this.bot.utils.wait(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 => {
|
|
||||||
// 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: number, element: any) => {
|
|
||||||
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: any) => 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
|
|
||||||
await saveSessionData(this.bot.config.sessionPath, browser, email, this.bot.isMobile)
|
|
||||||
|
|
||||||
await this.bot.utils.wait(2000)
|
|
||||||
|
|
||||||
// Close browser
|
|
||||||
await browser.close()
|
|
||||||
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
|
|
||||||
} catch (error) {
|
|
||||||
throw this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { 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<void> {
|
|
||||||
const buttons = [
|
|
||||||
{ 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' }
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const button of buttons) {
|
|
||||||
try {
|
|
||||||
const element = button.isXPath ? page.locator(`xpath=${button.selector}`) : page.locator(button.selector)
|
|
||||||
await element.first().click({ timeout: 500 })
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
|
|
||||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Silent fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle blocking Bing privacy overlay intercepting clicks (#bnp_overlay_wrapper)
|
|
||||||
try {
|
|
||||||
const overlay = await page.locator('#bnp_overlay_wrapper').first()
|
|
||||||
if (await overlay.isVisible({ timeout: 500 }).catch(()=>false)) {
|
|
||||||
// Try common dismiss buttons inside overlay
|
|
||||||
const rejectBtn = await page.locator('#bnp_btn_reject, button[aria-label*="Reject" i]').first()
|
|
||||||
const acceptBtn = await page.locator('#bnp_btn_accept').first()
|
|
||||||
if (await rejectBtn.isVisible().catch(()=>false)) {
|
|
||||||
await rejectBtn.click({ timeout: 500 }).catch(()=>{})
|
|
||||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Reject')
|
|
||||||
} else if (await acceptBtn.isVisible().catch(()=>false)) {
|
|
||||||
await acceptBtn.click({ timeout: 500 }).catch(()=>{})
|
|
||||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Accept (fallback)')
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"baseURL": "https://rewards.bing.com",
|
|
||||||
"sessionPath": "sessions",
|
|
||||||
"headless": false,
|
|
||||||
"parallel": false,
|
|
||||||
"runOnZeroPoints": false,
|
|
||||||
"clusters": 1,
|
|
||||||
"saveFingerprint": {
|
|
||||||
"mobile": false,
|
|
||||||
"desktop": false
|
|
||||||
},
|
|
||||||
"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": ""
|
|
||||||
},
|
|
||||||
"conclusionWebhook": {
|
|
||||||
"enabled": false,
|
|
||||||
"url": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs
|
|
||||||
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /proc/1/fd/1 2>&1
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Page } from 'rebrowser-playwright'
|
|
||||||
|
|
||||||
import { MicrosoftRewardsBot } from '../index'
|
|
||||||
|
|
||||||
import { Search } from './activities/Search'
|
|
||||||
import { ABC } from './activities/ABC'
|
|
||||||
import { Poll } from './activities/Poll'
|
|
||||||
import { Quiz } from './activities/Quiz'
|
|
||||||
import { ThisOrThat } from './activities/ThisOrThat'
|
|
||||||
import { UrlReward } from './activities/UrlReward'
|
|
||||||
import { SearchOnBing } from './activities/SearchOnBing'
|
|
||||||
import { ReadToEarn } from './activities/ReadToEarn'
|
|
||||||
import { DailyCheckIn } from './activities/DailyCheckIn'
|
|
||||||
|
|
||||||
import { DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
import type { Page } from '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({
|
|
||||||
// Use as any to avoid strict typing issues with our minimal process shim
|
|
||||||
input: (process as any).stdin,
|
|
||||||
output: (process as any).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'
|
|
||||||
// Flag to prevent spamming passkey logs after first handling
|
|
||||||
private passkeyHandled: boolean = false
|
|
||||||
|
|
||||||
constructor(bot: MicrosoftRewardsBot) {
|
|
||||||
this.bot = bot
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(page: Page, email: string, password: string) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Starting login process!')
|
|
||||||
|
|
||||||
// Navigate to the Bing login page
|
|
||||||
await page.goto('https://www.bing.com/rewards/dashboard')
|
|
||||||
|
|
||||||
// Disable FIDO support in login request
|
|
||||||
await page.route('**/GetCredentialType.srf*', (route: any) => {
|
|
||||||
const body = JSON.parse(route.request().postData() || '{}')
|
|
||||||
body.isFidoSupported = false
|
|
||||||
route.continue({ postData: JSON.stringify(body) })
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.waitForLoadState('domcontentloaded').catch(() => { })
|
|
||||||
|
|
||||||
await this.bot.browser.utils.reloadBadPage(page)
|
|
||||||
|
|
||||||
// Check if account is locked
|
|
||||||
await this.checkAccountLocked(page)
|
|
||||||
|
|
||||||
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
|
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
const emailInputSelector = 'input[type="email"]'
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for email field
|
|
||||||
const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).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('#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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).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) {
|
|
||||||
const passwordInputSelector = 'input[type="password"]'
|
|
||||||
const skip2FASelector = '#idA_PWD_SwitchToPassword';
|
|
||||||
try {
|
|
||||||
const skip2FAButton = await page.waitForSelector(skip2FASelector, { timeout: 2000 }).catch(() => null)
|
|
||||||
if (skip2FAButton) {
|
|
||||||
await skip2FAButton.click()
|
|
||||||
await this.bot.utils.wait(2000)
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipped 2FA')
|
|
||||||
} else {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'No 2FA skip button found, proceeding with password entry')
|
|
||||||
}
|
|
||||||
const viewFooter = await page.waitForSelector('#view > div > span:nth-child(6)', { timeout: 2000 }).catch(() => null)
|
|
||||||
const passwordField1 = await page.waitForSelector(passwordInputSelector, { timeout: 5000 }).catch(() => null)
|
|
||||||
if (viewFooter && !passwordField1) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page "Get a code to sign in" found by "viewFooter"')
|
|
||||||
|
|
||||||
const otherWaysButton = await viewFooter.$('span[role="button"]')
|
|
||||||
if (otherWaysButton) {
|
|
||||||
await otherWaysButton.click()
|
|
||||||
await this.bot.utils.wait(5000)
|
|
||||||
|
|
||||||
const secondListItem = page.locator('[role="listitem"]').nth(1)
|
|
||||||
if (await secondListItem.isVisible()) {
|
|
||||||
await secondListItem.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
|
|
||||||
if (nextButton) {
|
|
||||||
await nextButton.click()
|
|
||||||
await this.bot.utils.wait(2000)
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
|
|
||||||
} else {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
|
|
||||||
await this.handle2FA(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handle2FA(page: Page) {
|
|
||||||
try {
|
|
||||||
const numberToPress = await this.get2FACode(page)
|
|
||||||
if (numberToPress) {
|
|
||||||
// Authenticator 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, div[data-testid="displaySign"]>span', { 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, div[data-testid="displaySign"]>span', { 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('form[name="f1"]', { 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"]')
|
|
||||||
const primaryButton = await page.waitForSelector('button[data-testid="primaryButton"]', { state: 'visible', timeout: 5000 }).catch(() => null)
|
|
||||||
if (primaryButton) {
|
|
||||||
await primaryButton.click()
|
|
||||||
}
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Disable FIDO for OAuth flow as well (reduces passkey prompts resurfacing)
|
|
||||||
await page.route('**/GetCredentialType.srf*', (route: any) => {
|
|
||||||
const body = JSON.parse(route.request().postData() || '{}')
|
|
||||||
body.isFidoSupported = false
|
|
||||||
route.continue({ postData: JSON.stringify(body) })
|
|
||||||
}).catch(()=>{})
|
|
||||||
|
|
||||||
await page.goto(authorizeUrl.href)
|
|
||||||
|
|
||||||
let currentUrl = new URL(page.url())
|
|
||||||
let code: string
|
|
||||||
|
|
||||||
const authStart = Date.now()
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Waiting for authorization...')
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
// Attempt to dismiss passkey/passkey-like screens quickly (non-blocking)
|
|
||||||
await this.tryDismissPasskeyPrompt(page)
|
|
||||||
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
|
|
||||||
code = currentUrl.searchParams.get('code')!
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUrl = new URL(page.url())
|
|
||||||
// Shorter wait to react faster to passkey prompt
|
|
||||||
await this.bot.utils.wait(1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
const authDuration = Date.now() - authStart
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Successfully authorized in ${Math.round(authDuration/1000)}s`)
|
|
||||||
return tokenData.access_token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
|
|
||||||
private async checkLoggedIn(page: Page) {
|
|
||||||
const targetHostname = 'rewards.bing.com'
|
|
||||||
const targetPathname = '/'
|
|
||||||
|
|
||||||
const start = Date.now()
|
|
||||||
const maxWaitMs = Number(process.env.LOGIN_MAX_WAIT_MS || 180000) // default 3 minutes
|
|
||||||
let guidanceLogged = false
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
|
||||||
while (true) {
|
|
||||||
await this.dismissLoginMessages(page)
|
|
||||||
const currentURL = new URL(page.url())
|
|
||||||
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we keep looping without prompts for too long, advise and fail fast
|
|
||||||
const elapsed = Date.now() - start
|
|
||||||
if (elapsed > maxWaitMs) {
|
|
||||||
if (!guidanceLogged) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-GUIDE', 'Login taking too long without prompts.')
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-GUIDE', 'Tip: Enable passwordless sign-in (Microsoft Authenticator "number match") or add a TOTP secret in accounts.json to auto-fill OTP.')
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-GUIDE', 'You can also set LOGIN_MAX_WAIT_MS to increase this timeout if needed.')
|
|
||||||
guidanceLogged = true
|
|
||||||
}
|
|
||||||
throw this.bot.log(this.bot.isMobile, 'LOGIN-TIMEOUT', `Login timed out after ${Math.round(elapsed/1000)}s without completing`, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for login to complete
|
|
||||||
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal')
|
|
||||||
}
|
|
||||||
|
|
||||||
private lastNoPromptLog: number = 0
|
|
||||||
private noPromptIterations: number = 0
|
|
||||||
private async dismissLoginMessages(page: Page) {
|
|
||||||
let didSomething = false
|
|
||||||
|
|
||||||
// PASSKEY / Windows Hello / Sign in faster
|
|
||||||
const passkeyVideo = await page.waitForSelector('[data-testid="biometricVideo"]', { timeout: 1000 }).catch(() => null)
|
|
||||||
if (passkeyVideo) {
|
|
||||||
const skipButton = await page.$('button[data-testid="secondaryButton"]')
|
|
||||||
if (skipButton) {
|
|
||||||
await skipButton.click().catch(()=>{})
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Passkey dialog detected (video heuristic) -> clicked "Skip for now"')
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
didSomething = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!didSomething) {
|
|
||||||
const titleEl = await page.waitForSelector('[data-testid="title"]', { timeout: 800 }).catch(() => null)
|
|
||||||
const titleText = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
|
|
||||||
const looksLikePasskey = /sign in faster|passkey|fingerprint|face|pin/i.test(titleText)
|
|
||||||
const secondaryBtn = await page.waitForSelector('button[data-testid="secondaryButton"]', { timeout: 500 }).catch(() => null)
|
|
||||||
const primaryBtn = await page.waitForSelector('button[data-testid="primaryButton"]', { timeout: 500 }).catch(() => null)
|
|
||||||
if (looksLikePasskey && secondaryBtn) {
|
|
||||||
await secondaryBtn.click().catch(()=>{})
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Passkey dialog detected (title: "${titleText}") -> clicked secondary`)
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
didSomething = true
|
|
||||||
} else if (!didSomething && secondaryBtn && primaryBtn) {
|
|
||||||
const secText = (await secondaryBtn.textContent() || '').trim()
|
|
||||||
if (/skip for now/i.test(secText)) {
|
|
||||||
await secondaryBtn.click().catch(()=>{})
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Passkey dialog (pair heuristic) -> clicked secondary (Skip for now)')
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
didSomething = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!didSomething) {
|
|
||||||
const skipByText = await page.locator('xpath=//button[contains(normalize-space(.), "Skip for now")]').first()
|
|
||||||
if (await skipByText.isVisible().catch(()=>false)) {
|
|
||||||
await skipByText.click().catch(()=>{})
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Passkey dialog (text fallback) -> clicked "Skip for now"')
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
didSomething = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!didSomething) {
|
|
||||||
const closeBtn = await page.$('#close-button')
|
|
||||||
if (closeBtn) {
|
|
||||||
await closeBtn.click().catch(()=>{})
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', 'Attempted close button on potential passkey modal')
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSI (Keep me signed in) prompt
|
|
||||||
const kmsi = await page.waitForSelector('[data-testid="kmsiVideo"]', { timeout: 800 }).catch(()=>null)
|
|
||||||
if (kmsi) {
|
|
||||||
const yesButton = await page.$('button[data-testid="primaryButton"]')
|
|
||||||
if (yesButton) {
|
|
||||||
await yesButton.click().catch(()=>{})
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-KMSI', 'KMSI dialog detected -> accepted (Yes)')
|
|
||||||
await page.waitForTimeout(300)
|
|
||||||
didSomething = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!didSomething) {
|
|
||||||
this.noPromptIterations++
|
|
||||||
const now = Date.now()
|
|
||||||
if (this.noPromptIterations === 1 || (now - this.lastNoPromptLog) > 10000) {
|
|
||||||
this.lastNoPromptLog = now
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-NO-PROMPT', `No dialogs (x${this.noPromptIterations})`)
|
|
||||||
// Reset counter if it grows large to keep number meaningful
|
|
||||||
if (this.noPromptIterations > 50) this.noPromptIterations = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset counters after an interaction
|
|
||||||
this.noPromptIterations = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lightweight passkey prompt dismissal used in mobile OAuth loop */
|
|
||||||
private async tryDismissPasskeyPrompt(page: Page) {
|
|
||||||
try {
|
|
||||||
// Fast existence checks with very small timeouts to avoid slowing the loop
|
|
||||||
const titleEl = await page.waitForSelector('[data-testid="title"]', { timeout: 500 }).catch(() => null)
|
|
||||||
const secondaryBtn = await page.waitForSelector('button[data-testid="secondaryButton"]', { timeout: 500 }).catch(() => null)
|
|
||||||
// Direct text locator fallback (sometimes data-testid changes)
|
|
||||||
const textSkip = secondaryBtn ? null : await page.locator('xpath=//button[contains(normalize-space(.), "Skip for now")]').first().isVisible().catch(()=>false)
|
|
||||||
if (secondaryBtn) {
|
|
||||||
// Heuristic: if title indicates passkey or both primary/secondary exist with typical text
|
|
||||||
let shouldClick = false
|
|
||||||
let titleText = ''
|
|
||||||
if (titleEl) {
|
|
||||||
titleText = (await titleEl.textContent() || '').trim()
|
|
||||||
if (/sign in faster|passkey|fingerprint|face|pin/i.test(titleText)) {
|
|
||||||
shouldClick = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!shouldClick && textSkip) {
|
|
||||||
shouldClick = true
|
|
||||||
}
|
|
||||||
if (!shouldClick) {
|
|
||||||
// Fallback text probe on the secondary button itself
|
|
||||||
const btnText = (await secondaryBtn.textContent() || '').trim()
|
|
||||||
if (/skip for now/i.test(btnText)) {
|
|
||||||
shouldClick = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldClick) {
|
|
||||||
await secondaryBtn.click().catch(() => { })
|
|
||||||
if (!this.passkeyHandled) {
|
|
||||||
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Passkey prompt (loop) -> clicked skip${titleText ? ` (title: ${titleText})` : ''}`)
|
|
||||||
}
|
|
||||||
this.passkeyHandled = true
|
|
||||||
await this.bot.utils.wait(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { /* ignore minor errors */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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: any) => 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: any) => 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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
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: number) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import type { Page } from '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 this.safeClick(page, 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 safeClick(page: Page, selector: string) {
|
|
||||||
try {
|
|
||||||
await page.click(selector, { timeout: 5000 })
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = (e?.message || '')
|
|
||||||
if (/Timeout.*click/i.test(msg) || /intercepts pointer events/i.test(msg)) {
|
|
||||||
// Try to dismiss overlays then retry once
|
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
|
||||||
await this.bot.utils.wait(500)
|
|
||||||
await page.click(selector, { timeout: 5000 })
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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é"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
548
src/index.ts
548
src/index.ts
@@ -1,548 +0,0 @@
|
|||||||
import cluster from 'cluster'
|
|
||||||
// Use Page type from playwright for typings; at runtime rebrowser-playwright extends playwright
|
|
||||||
import type { Page } from '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'
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
|
|
||||||
// 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 = ''
|
|
||||||
|
|
||||||
// Summary collection (per process)
|
|
||||||
private accountSummaries: AccountSummary[] = []
|
|
||||||
|
|
||||||
//@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() {
|
|
||||||
this.printBanner()
|
|
||||||
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 printBanner() {
|
|
||||||
// Only print once (primary process or single cluster execution)
|
|
||||||
if (this.config.clusters > 1 && !cluster.isPrimary) return
|
|
||||||
try {
|
|
||||||
const pkgPath = path.join(__dirname, '../', 'package.json')
|
|
||||||
let version = 'unknown'
|
|
||||||
if (fs.existsSync(pkgPath)) {
|
|
||||||
const raw = fs.readFileSync(pkgPath, 'utf-8')
|
|
||||||
const pkg = JSON.parse(raw)
|
|
||||||
version = pkg.version || version
|
|
||||||
}
|
|
||||||
const banner = [
|
|
||||||
' __ __ _____ _____ _ ',
|
|
||||||
' | \/ |/ ____| | __ \\ | | ',
|
|
||||||
' | \ / | (___ ______| |__) |_____ ____ _ _ __ __| |___ ',
|
|
||||||
' | |\/| |\\___ \\______| _ // _ \\ \\ /\\ / / _` | \'__/ _` / __|',
|
|
||||||
' | | | |____) | | | \\ \\ __/ \\ V V / (_| | | | (_| \\__ \\',
|
|
||||||
' |_| |_|_____/ |_| \\_\\___| \\_/\\_/ \\__,_|_| \\__,_|___/',
|
|
||||||
'',
|
|
||||||
` Version: v${version}`,
|
|
||||||
''
|
|
||||||
].join('\n')
|
|
||||||
console.log(banner)
|
|
||||||
} catch { /* ignore banner errors */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return summaries (used when clusters==1)
|
|
||||||
public getSummaries() {
|
|
||||||
return this.accountSummaries
|
|
||||||
}
|
|
||||||
|
|
||||||
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 as any).send?.({ chunk })
|
|
||||||
// Collect summaries from workers
|
|
||||||
worker.on('message', (msg: any) => {
|
|
||||||
if (msg && msg.type === 'summary' && Array.isArray(msg.data)) {
|
|
||||||
this.accountSummaries.push(...msg.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
cluster.on('exit', (worker: any, code: number) => {
|
|
||||||
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) {
|
|
||||||
// All workers done -> send conclusion (if enabled) then exit
|
|
||||||
this.sendConclusion(this.accountSummaries).finally(() => {
|
|
||||||
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 as any).on('message', async ({ chunk }: { chunk: Account[] }) => {
|
|
||||||
await this.runTasks(chunk)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runTasks(accounts: Account[]) {
|
|
||||||
for (const account of accounts) {
|
|
||||||
log('main', 'MAIN-WORKER', `Started tasks for account ${account.email}`)
|
|
||||||
|
|
||||||
const accountStart = Date.now()
|
|
||||||
let desktopInitial = 0
|
|
||||||
let mobileInitial = 0
|
|
||||||
let desktopCollected = 0
|
|
||||||
let mobileCollected = 0
|
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
this.axios = new Axios(account.proxy)
|
|
||||||
const verbose = process.env.DEBUG_REWARDS_VERBOSE === '1'
|
|
||||||
const formatFullErr = (label: string, e: any) => {
|
|
||||||
const base = shortErr(e)
|
|
||||||
if (verbose && e instanceof Error) {
|
|
||||||
return `${label}:${base} :: ${e.stack?.split('\n').slice(0,4).join(' | ')}`
|
|
||||||
}
|
|
||||||
return `${label}:${base}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.parallel) {
|
|
||||||
const mobileInstance = new MicrosoftRewardsBot(true)
|
|
||||||
mobileInstance.axios = this.axios
|
|
||||||
// Run both and capture results with detailed logging
|
|
||||||
const desktopPromise = this.Desktop(account).catch(e => {
|
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
|
||||||
errors.push(formatFullErr('desktop', e)); return null
|
|
||||||
})
|
|
||||||
const mobilePromise = mobileInstance.Mobile(account).catch(e => {
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
|
||||||
errors.push(formatFullErr('mobile', e)); return null
|
|
||||||
})
|
|
||||||
const [desktopResult, mobileResult] = await Promise.all([desktopPromise, mobilePromise])
|
|
||||||
if (desktopResult) {
|
|
||||||
desktopInitial = desktopResult.initialPoints
|
|
||||||
desktopCollected = desktopResult.collectedPoints
|
|
||||||
}
|
|
||||||
if (mobileResult) {
|
|
||||||
mobileInitial = mobileResult.initialPoints
|
|
||||||
mobileCollected = mobileResult.collectedPoints
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.isMobile = false
|
|
||||||
const desktopResult = await this.Desktop(account).catch(e => {
|
|
||||||
log(false, 'TASK', `Desktop flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
|
||||||
errors.push(formatFullErr('desktop', e)); return null
|
|
||||||
})
|
|
||||||
if (desktopResult) {
|
|
||||||
desktopInitial = desktopResult.initialPoints
|
|
||||||
desktopCollected = desktopResult.collectedPoints
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isMobile = true
|
|
||||||
const mobileResult = await this.Mobile(account).catch(e => {
|
|
||||||
log(true, 'TASK', `Mobile flow failed early for ${account.email}: ${e instanceof Error ? e.message : e}`,'error')
|
|
||||||
errors.push(formatFullErr('mobile', e)); return null
|
|
||||||
})
|
|
||||||
if (mobileResult) {
|
|
||||||
mobileInitial = mobileResult.initialPoints
|
|
||||||
mobileCollected = mobileResult.collectedPoints
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountEnd = Date.now()
|
|
||||||
const durationMs = accountEnd - accountStart
|
|
||||||
const totalCollected = desktopCollected + mobileCollected
|
|
||||||
const initialTotal = (desktopInitial || 0) + (mobileInitial || 0)
|
|
||||||
this.accountSummaries.push({
|
|
||||||
email: account.email,
|
|
||||||
durationMs,
|
|
||||||
desktopCollected,
|
|
||||||
mobileCollected,
|
|
||||||
totalCollected,
|
|
||||||
initialTotal,
|
|
||||||
endTotal: initialTotal + totalCollected,
|
|
||||||
errors
|
|
||||||
})
|
|
||||||
|
|
||||||
log('main', 'MAIN-WORKER', `Completed tasks for account ${account.email}`, 'log', 'green')
|
|
||||||
}
|
|
||||||
|
|
||||||
log(this.isMobile, 'MAIN-PRIMARY', 'Completed tasks for ALL accounts', 'log', 'green')
|
|
||||||
// Extra diagnostic summary when verbose
|
|
||||||
if (process.env.DEBUG_REWARDS_VERBOSE === '1') {
|
|
||||||
for (const summary of this.accountSummaries) {
|
|
||||||
log('main','SUMMARY-DEBUG',`Account ${summary.email} collected D:${summary.desktopCollected} M:${summary.mobileCollected} TOTAL:${summary.totalCollected} ERRORS:${summary.errors.length ? summary.errors.join(';') : 'none'}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If in worker mode (clusters>1) send summaries to primary
|
|
||||||
if (this.config.clusters > 1 && !cluster.isPrimary) {
|
|
||||||
if (process.send) {
|
|
||||||
process.send({ type: 'summary', data: this.accountSummaries })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single process mode -> build and send conclusion directly
|
|
||||||
await this.sendConclusion(this.accountSummaries)
|
|
||||||
}
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop
|
|
||||||
async Desktop(account: Account) {
|
|
||||||
log(false,'FLOW','Desktop() invoked')
|
|
||||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
|
||||||
this.homePage = await browser.newPage()
|
|
||||||
|
|
||||||
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
|
|
||||||
const initial = this.pointsInitial
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Fetch points BEFORE closing (avoid page closed reload error)
|
|
||||||
const after = await this.browser.func.getCurrentPoints().catch(()=>initial)
|
|
||||||
// Close desktop browser
|
|
||||||
await this.browser.func.closeBrowser(browser, account.email)
|
|
||||||
return {
|
|
||||||
initialPoints: initial,
|
|
||||||
collectedPoints: (after - initial) || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile
|
|
||||||
async Mobile(account: Account) {
|
|
||||||
log(true,'FLOW','Mobile() invoked')
|
|
||||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
|
||||||
this.homePage = await browser.newPage()
|
|
||||||
|
|
||||||
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 initialPoints = data.userStatus.availablePoints || this.pointsInitial || 0
|
|
||||||
|
|
||||||
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 {
|
|
||||||
initialPoints: initialPoints,
|
|
||||||
collectedPoints: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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 - initialPoints} points today`)
|
|
||||||
|
|
||||||
// Close mobile browser
|
|
||||||
await this.browser.func.closeBrowser(browser, account.email)
|
|
||||||
return {
|
|
||||||
initialPoints: initialPoints,
|
|
||||||
collectedPoints: (afterPointAmount - initialPoints) || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendConclusion(summaries: AccountSummary[]) {
|
|
||||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
|
||||||
const cfg = this.config
|
|
||||||
if (!cfg.conclusionWebhook || !cfg.conclusionWebhook.enabled) return
|
|
||||||
|
|
||||||
const totalAccounts = summaries.length
|
|
||||||
if (totalAccounts === 0) return
|
|
||||||
|
|
||||||
let totalCollected = 0
|
|
||||||
let totalInitial = 0
|
|
||||||
let totalEnd = 0
|
|
||||||
let totalDuration = 0
|
|
||||||
let accountsWithErrors = 0
|
|
||||||
|
|
||||||
const accountFields: any[] = []
|
|
||||||
for (const s of summaries) {
|
|
||||||
totalCollected += s.totalCollected
|
|
||||||
totalInitial += s.initialTotal
|
|
||||||
totalEnd += s.endTotal
|
|
||||||
totalDuration += s.durationMs
|
|
||||||
if (s.errors.length) accountsWithErrors++
|
|
||||||
|
|
||||||
const statusEmoji = s.errors.length ? '⚠️' : '✅'
|
|
||||||
const diff = s.totalCollected
|
|
||||||
const duration = formatDuration(s.durationMs)
|
|
||||||
const valueLines: string[] = [
|
|
||||||
`Points: ${s.initialTotal} → ${s.endTotal} ( +${diff} )`,
|
|
||||||
`Breakdown: 🖥️ ${s.desktopCollected} | 📱 ${s.mobileCollected}`,
|
|
||||||
`Duration: ⏱️ ${duration}`
|
|
||||||
]
|
|
||||||
if (s.errors.length) {
|
|
||||||
valueLines.push(`Errors: ${s.errors.slice(0,2).join(' | ')}`)
|
|
||||||
}
|
|
||||||
accountFields.push({
|
|
||||||
name: `${statusEmoji} ${s.email}`.substring(0, 256),
|
|
||||||
value: valueLines.join('\n').substring(0, 1024),
|
|
||||||
inline: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgDuration = totalDuration / totalAccounts
|
|
||||||
const embed = {
|
|
||||||
title: '🎯 Microsoft Rewards Summary',
|
|
||||||
description: `Processed **${totalAccounts}** account(s)${accountsWithErrors ? ` • ${accountsWithErrors} with issues` : ''}`,
|
|
||||||
color: accountsWithErrors ? 0xFFAA00 : 0x32CD32,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'Global Totals',
|
|
||||||
value: [
|
|
||||||
`Total Points: ${totalInitial} → ${totalEnd} ( +${totalCollected} )`,
|
|
||||||
`Average Duration: ${formatDuration(avgDuration)}`,
|
|
||||||
`Cumulative Runtime: ${formatDuration(totalDuration)}`
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
...accountFields
|
|
||||||
].slice(0, 25), // Discord max 25 fields
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
footer: {
|
|
||||||
text: 'Script conclusion webhook'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback plain text (rare) & embed send
|
|
||||||
const fallback = `Microsoft Rewards Summary\nAccounts: ${totalAccounts}\nTotal: ${totalInitial} -> ${totalEnd} (+${totalCollected})\nRuntime: ${formatDuration(totalDuration)}`
|
|
||||||
await ConclusionWebhook(cfg, fallback, { embeds: [embed] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AccountSummary {
|
|
||||||
email: string
|
|
||||||
durationMs: number
|
|
||||||
desktopCollected: number
|
|
||||||
mobileCollected: number
|
|
||||||
totalCollected: number
|
|
||||||
initialTotal: number
|
|
||||||
endTotal: number
|
|
||||||
errors: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortErr(e: any): string {
|
|
||||||
if (!e) return 'unknown'
|
|
||||||
if (e instanceof Error) return e.message.substring(0, 120)
|
|
||||||
const s = String(e)
|
|
||||||
return s.substring(0, 120)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (!ms || ms < 1000) return `${ms}ms`
|
|
||||||
const sec = Math.floor(ms / 1000)
|
|
||||||
const h = Math.floor(sec / 3600)
|
|
||||||
const m = Math.floor((sec % 3600) / 60)
|
|
||||||
const s = sec % 60
|
|
||||||
const parts: string[] = []
|
|
||||||
if (h) parts.push(`${h}h`)
|
|
||||||
if (m) parts.push(`${m}m`)
|
|
||||||
if (s) parts.push(`${s}s`)
|
|
||||||
return parts.join(' ') || `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export interface Account {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
proxy: AccountProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountProxy {
|
|
||||||
proxyAxios: boolean;
|
|
||||||
url: string;
|
|
||||||
port: number;
|
|
||||||
password: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
export interface Config {
|
|
||||||
baseURL: string;
|
|
||||||
sessionPath: string;
|
|
||||||
headless: boolean;
|
|
||||||
parallel: boolean;
|
|
||||||
runOnZeroPoints: boolean;
|
|
||||||
clusters: number;
|
|
||||||
saveFingerprint: ConfigSaveFingerprint;
|
|
||||||
workers: ConfigWorkers;
|
|
||||||
searchOnBingLocalQueries: boolean;
|
|
||||||
globalTimeout: number | string;
|
|
||||||
searchSettings: ConfigSearchSettings;
|
|
||||||
logExcludeFunc: string[];
|
|
||||||
webhookLogExcludeFunc: string[];
|
|
||||||
proxy: ConfigProxy;
|
|
||||||
webhook: ConfigWebhook;
|
|
||||||
conclusionWebhook?: ConfigWebhook; // Optional secondary webhook for final summary
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,701 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface OAuth {
|
|
||||||
access_token: string;
|
|
||||||
refresh_token: string;
|
|
||||||
scope: string;
|
|
||||||
expires_in: number;
|
|
||||||
ext_expires_in: number;
|
|
||||||
foci: string;
|
|
||||||
token_type: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface EarnablePoints {
|
|
||||||
desktopSearchPoints: number
|
|
||||||
mobileSearchPoints: number
|
|
||||||
dailySetPoints: number
|
|
||||||
morePromotionsPoints: number
|
|
||||||
totalEarnablePoints: number
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface GoogleSearch {
|
|
||||||
topic: string;
|
|
||||||
related: string[];
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// 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'
|
|
||||||
}
|
|
||||||
156
src/run_daily.sh
156
src/run_daily.sh
@@ -1,156 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
export PATH="/usr/local/bin:/usr/bin:/bin"
|
|
||||||
export PLAYWRIGHT_BROWSERS_PATH=0
|
|
||||||
export TZ="${TZ:-UTC}"
|
|
||||||
|
|
||||||
cd /usr/src/microsoft-rewards-script
|
|
||||||
|
|
||||||
LOCKFILE=/tmp/run_daily.lock
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# Function: Check and fix lockfile integrity
|
|
||||||
# -------------------------------
|
|
||||||
self_heal_lockfile() {
|
|
||||||
# If lockfile exists but is empty → remove it
|
|
||||||
if [ -f "$LOCKFILE" ]; then
|
|
||||||
local lock_content
|
|
||||||
lock_content=$(<"$LOCKFILE" || echo "")
|
|
||||||
|
|
||||||
if [[ -z "$lock_content" ]]; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Found empty lockfile → removing."
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If lockfile contains non-numeric PID → remove it
|
|
||||||
if ! [[ "$lock_content" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Found corrupted lockfile content ('$lock_content') → removing."
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If lockfile contains PID but process is dead → remove it
|
|
||||||
if ! kill -0 "$lock_content" 2>/dev/null; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Lockfile PID $lock_content is dead → removing stale lock."
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# Function: Acquire lock
|
|
||||||
# -------------------------------
|
|
||||||
acquire_lock() {
|
|
||||||
local max_attempts=5
|
|
||||||
local attempt=0
|
|
||||||
local timeout_hours=${STUCK_PROCESS_TIMEOUT_HOURS:-8}
|
|
||||||
local timeout_seconds=$((timeout_hours * 3600))
|
|
||||||
|
|
||||||
while [ $attempt -lt $max_attempts ]; do
|
|
||||||
# Try to create lock with current PID
|
|
||||||
if (set -C; echo "$$" > "$LOCKFILE") 2>/dev/null; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Lock acquired successfully (PID: $$)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Lock exists, validate it
|
|
||||||
if [ -f "$LOCKFILE" ]; then
|
|
||||||
local existing_pid
|
|
||||||
existing_pid=$(<"$LOCKFILE" || echo "")
|
|
||||||
|
|
||||||
echo "[$(date)] [run_daily.sh] Lock file exists with PID: '$existing_pid'"
|
|
||||||
|
|
||||||
# If lockfile content is invalid → delete and retry
|
|
||||||
if [[ -z "$existing_pid" || ! "$existing_pid" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Removing invalid lockfile → retrying..."
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If process is dead → delete and retry
|
|
||||||
if ! kill -0 "$existing_pid" 2>/dev/null; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Removing stale lock (dead PID: $existing_pid)"
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check process runtime → kill if exceeded timeout
|
|
||||||
local process_age
|
|
||||||
if process_age=$(ps -o etimes= -p "$existing_pid" 2>/dev/null | tr -d ' '); then
|
|
||||||
if [ "$process_age" -gt "$timeout_seconds" ]; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Killing stuck process $existing_pid (${process_age}s > ${timeout_hours}h)"
|
|
||||||
kill -TERM "$existing_pid" 2>/dev/null || true
|
|
||||||
sleep 5
|
|
||||||
kill -KILL "$existing_pid" 2>/dev/null || true
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$(date)] [run_daily.sh] Lock held by PID $existing_pid, attempt $((attempt + 1))/$max_attempts"
|
|
||||||
sleep 2
|
|
||||||
((attempt++))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "[$(date)] [run_daily.sh] Could not acquire lock after $max_attempts attempts; exiting."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# Function: Release lock
|
|
||||||
# -------------------------------
|
|
||||||
release_lock() {
|
|
||||||
if [ -f "$LOCKFILE" ]; then
|
|
||||||
local lock_pid
|
|
||||||
lock_pid=$(<"$LOCKFILE")
|
|
||||||
if [ "$lock_pid" = "$$" ]; then
|
|
||||||
rm -f "$LOCKFILE"
|
|
||||||
echo "[$(date)] [run_daily.sh] Lock released (PID: $$)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Always release lock on exit — but only if we acquired it
|
|
||||||
trap 'release_lock' EXIT INT TERM
|
|
||||||
|
|
||||||
# -------------------------------
|
|
||||||
# MAIN EXECUTION FLOW
|
|
||||||
# -------------------------------
|
|
||||||
echo "[$(date)] [run_daily.sh] Current process PID: $$"
|
|
||||||
|
|
||||||
# Self-heal any broken or empty locks before proceeding
|
|
||||||
self_heal_lockfile
|
|
||||||
|
|
||||||
# Attempt to acquire the lock safely
|
|
||||||
if ! acquire_lock; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Random sleep between MIN and MAX to spread execution
|
|
||||||
MINWAIT=${MIN_SLEEP_MINUTES:-5}
|
|
||||||
MAXWAIT=${MAX_SLEEP_MINUTES:-50}
|
|
||||||
MINWAIT_SEC=$((MINWAIT*60))
|
|
||||||
MAXWAIT_SEC=$((MAXWAIT*60))
|
|
||||||
|
|
||||||
if [ "${SKIP_RANDOM_SLEEP:-false}" != "true" ]; then
|
|
||||||
SLEEPTIME=$(( MINWAIT_SEC + RANDOM % (MAXWAIT_SEC - MINWAIT_SEC) ))
|
|
||||||
echo "[$(date)] [run_daily.sh] Sleeping for $((SLEEPTIME/60)) minutes ($SLEEPTIME seconds)"
|
|
||||||
sleep "$SLEEPTIME"
|
|
||||||
else
|
|
||||||
echo "[$(date)] [run_daily.sh] Skipping random sleep"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Start the actual script
|
|
||||||
echo "[$(date)] [run_daily.sh] Starting script..."
|
|
||||||
if npm start; then
|
|
||||||
echo "[$(date)] [run_daily.sh] Script completed successfully."
|
|
||||||
else
|
|
||||||
echo "[$(date)] [run_daily.sh] ERROR: Script failed!" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$(date)] [run_daily.sh] Script finished"
|
|
||||||
# Lock is released automatically via trap
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import { Config } from '../interface/Config'
|
|
||||||
|
|
||||||
interface ConclusionPayload {
|
|
||||||
content?: string
|
|
||||||
embeds?: any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a final structured summary to the dedicated conclusion webhook (if enabled),
|
|
||||||
* otherwise do nothing. Does NOT fallback to the normal logging webhook to avoid spam.
|
|
||||||
*/
|
|
||||||
export async function ConclusionWebhook(configData: Config, content: string, embed?: ConclusionPayload) {
|
|
||||||
const webhook = configData.conclusionWebhook
|
|
||||||
|
|
||||||
if (!webhook || !webhook.enabled || webhook.url.length < 10) return
|
|
||||||
|
|
||||||
const body: ConclusionPayload = embed?.embeds ? { embeds: embed.embeds } : { content }
|
|
||||||
if (content && !body.content && !body.embeds) body.content = content
|
|
||||||
|
|
||||||
const request = {
|
|
||||||
method: 'POST',
|
|
||||||
url: webhook.url,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
data: body
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios(request).catch(() => { })
|
|
||||||
}
|
|
||||||
116
src/util/Load.ts
116
src/util/Load.ts
@@ -1,116 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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(() => { })
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"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). */
|
|
||||||
"types": ["node"],
|
|
||||||
"typeRoots": ["./node_modules/@types"],
|
|
||||||
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
|
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "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