mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-18 05:53:57 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c01fd433 | ||
|
|
00bf632ac7 | ||
|
|
3375fe6382 | ||
|
|
97c705cd7f | ||
|
|
f0bee7db5d | ||
|
|
cbd8842f2f | ||
|
|
a03bc903ad | ||
|
|
2f9c88f8d8 | ||
|
|
032debed62 | ||
|
|
ca3253ac52 | ||
|
|
171521c51f | ||
|
|
8365a0c422 | ||
|
|
62eb1775e3 | ||
|
|
576899f39d |
@@ -1,2 +1,13 @@
|
|||||||
dist
|
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
sessions/
|
||||||
|
.dev/
|
||||||
|
diagnostics/
|
||||||
|
note
|
||||||
|
accounts.dev.json
|
||||||
|
accounts.main.json
|
||||||
|
.playwright-chromium-installed
|
||||||
|
|||||||
71
.github/workflows/auto-release.yml
vendored
Normal file
71
.github/workflows/auto-release.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Auto Release on package.json version bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [v3]
|
||||||
|
paths:
|
||||||
|
- package.json
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Read current version
|
||||||
|
id: current
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Read previous version (from previous commit)
|
||||||
|
id: previous
|
||||||
|
run: |
|
||||||
|
PREV_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" || echo "")
|
||||||
|
echo "version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check if version increased
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
CUR="${{ steps.current.outputs.version }}"
|
||||||
|
PREV="${{ steps.previous.outputs.version }}"
|
||||||
|
|
||||||
|
echo "Current: $CUR"
|
||||||
|
echo "Previous: $PREV"
|
||||||
|
|
||||||
|
# if previous doesn't exist (first commit containing package.json), skip
|
||||||
|
if [ -z "$PREV" ]; then
|
||||||
|
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare semver using node's semver package if available; fallback to simple inequality
|
||||||
|
node -e "
|
||||||
|
const cur='${CUR}';
|
||||||
|
const prev='${PREV}';
|
||||||
|
let should = cur !== prev;
|
||||||
|
try {
|
||||||
|
const semver = require('semver');
|
||||||
|
should = semver.gt(cur, prev);
|
||||||
|
} catch (_) {}
|
||||||
|
console.log('should_release=' + should);
|
||||||
|
" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Stop if no bump
|
||||||
|
if: steps.check.outputs.should_release != 'true'
|
||||||
|
run: echo "No version increase detected."
|
||||||
|
|
||||||
|
- name: Create tag + GitHub Release
|
||||||
|
if: steps.check.outputs.should_release == 'true'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: v${{ steps.current.outputs.version }}
|
||||||
|
name: v${{ steps.current.outputs.version }}
|
||||||
|
generate_release_notes: true
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ accounts.dev.json
|
|||||||
accounts.main.json
|
accounts.main.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.playwright-chromium-installed
|
.playwright-chromium-installed
|
||||||
|
bun.lock
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
###############################################################################
|
###############################################################################
|
||||||
FROM node:22-slim AS builder
|
FROM node:24-slim AS builder
|
||||||
|
|
||||||
WORKDIR /usr/src/microsoft-rewards-script
|
WORKDIR /usr/src/microsoft-rewards-script
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ RUN npx patchright install --with-deps --only-shell chromium \
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Stage 2: Runtime
|
# Stage 2: Runtime
|
||||||
###############################################################################
|
###############################################################################
|
||||||
FROM node:22-slim AS runtime
|
FROM node:24-slim AS runtime
|
||||||
|
|
||||||
WORKDIR /usr/src/microsoft-rewards-script
|
WORKDIR /usr/src/microsoft-rewards-script
|
||||||
|
|
||||||
|
|||||||
213
README.md
213
README.md
@@ -2,9 +2,218 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
TODO
|
## Table of Contents
|
||||||
|
- [Quick Setup](#quick-setup)
|
||||||
|
- [Nix Setup](#nix-setup)
|
||||||
|
- [Configuration Options](#configuration-options)
|
||||||
|
- [Account Setup](#account-setup)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Disclaimer](#disclaimer)
|
||||||
|
|
||||||
[For installation see the main (v1) or v2 branch (mostly the same)](https://github.com/TheNetsky/Microsoft-Rewards-Script/tree/main?tab=readme-ov-file#setup)
|
---
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
**Requirements:** Node.js >= 24 and Git
|
||||||
|
Works on Windows, Linux, macOS, and WSL.
|
||||||
|
|
||||||
|
### Get the script
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/TheNetsky/Microsoft-Rewards-Script.git
|
||||||
|
cd Microsoft-Rewards-Script
|
||||||
|
```
|
||||||
|
Or, download the latest release ZIP and extract it.
|
||||||
|
|
||||||
|
### Create an account.json and config.json
|
||||||
|
Copy, rename, and edit your account and configuration files before deploying the script.
|
||||||
|
- Copy or rename `src/accounts.example.json` to `src/accounts.json` and add your credentials
|
||||||
|
- Copy or rename `src/config.example.json` to `src/config.json` and customize your preferences.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Do not skip this step.
|
||||||
|
> Prior versions of accounts.json and config.json are not compatible with current release.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> You must rebuild your script after making any changes to accounts.json and config.json.
|
||||||
|
|
||||||
|
### Build and run the script (bare metal version)
|
||||||
|
```bash
|
||||||
|
npm run pre-build
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build and run the script (docker version)
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Set `headless` to `true` in the `src/config.json` when using Docker.
|
||||||
|
> Additional docker-specific scheduling options are in the `compose.yaml`
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> When headeless, monitor logs with `docker logs microsoft-rewards-script` (for example, to view passwordless codes), or enable a webhook service in the `src/config.json`.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nix Setup
|
||||||
|
|
||||||
|
If using Nix: `bash scripts/nix/run.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
Edit `src/config.json` to customize behavior. Below are all currently available options.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Rebuild the script after all changes.
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
| `runOnZeroPoints` | boolean | `false` | Run even when no points are available |
|
||||||
|
| `clusters` | number | `1` | Number of concurrent account clusters |
|
||||||
|
| `errorDiagnostics` | boolean | `false` | Enable error diagnostics |
|
||||||
|
| `searchOnBingLocalQueries` | boolean | `false` | Use local query list |
|
||||||
|
| `globalTimeout` | string | `"30sec"` | Timeout for all actions |
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Set `headless` to `true` when using docker
|
||||||
|
|
||||||
|
### Workers
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|----------|------|----------|-------------|
|
||||||
|
| `workers.doDailySet` | boolean | `true` | Complete daily set |
|
||||||
|
| `workers.doSpecialPromotions` | boolean | `true` | Complete special promotions |
|
||||||
|
| `workers.doMorePromotions` | boolean | `true` | Complete more promotions |
|
||||||
|
| `workers.doPunchCards` | boolean | `true` | Complete punchcards |
|
||||||
|
| `workers.doAppPromotions` | boolean | `true` | Complete app promotions |
|
||||||
|
| `workers.doDesktopSearch` | boolean | `true` | Perform desktop searches |
|
||||||
|
| `workers.doMobileSearch` | boolean | `true` | Perform mobile searches |
|
||||||
|
| `workers.doDailyCheckIn` | boolean | `true` | Complete daily check-in |
|
||||||
|
| `workers.doReadToEarn` | boolean | `true` | Complete Read-to-Earn |
|
||||||
|
|
||||||
|
### Search Settings
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|----------|------|----------|-------------|
|
||||||
|
| `searchSettings.scrollRandomResults` | boolean | `false` | Scroll randomly on results |
|
||||||
|
| `searchSettings.clickRandomResults` | boolean | `false` | Click random links |
|
||||||
|
| `searchSettings.parallelSearching` | boolean | `true` | Run searches in parallel |
|
||||||
|
| `searchSettings.queryEngines` | string[] | `["google", "wikipedia", "reddit", "local"]` | Query engines to use |
|
||||||
|
| `searchSettings.searchResultVisitTime` | string | `"10sec"` | Time to spend on each search result |
|
||||||
|
| `searchSettings.searchDelay.min` | string | `"30sec"` | Minimum delay between searches |
|
||||||
|
| `searchSettings.searchDelay.max` | string | `"1min"` | Maximum delay between searches |
|
||||||
|
| `searchSettings.readDelay.min` | string | `"30sec"` | Minimum delay for reading |
|
||||||
|
| `searchSettings.readDelay.max` | string | `"1min"` | Maximum delay for reading |
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|----------|------|----------|-------------|
|
||||||
|
| `debugLogs` | boolean | `false` | Enable debug logging |
|
||||||
|
| `consoleLogFilter.enabled` | boolean | `false` | Enable console log filtering |
|
||||||
|
| `consoleLogFilter.mode` | string | `"whitelist"` | Filter mode (whitelist/blacklist) |
|
||||||
|
| `consoleLogFilter.levels` | string[] | `["error", "warn"]` | Log levels to filter |
|
||||||
|
| `consoleLogFilter.keywords` | string[] | `["starting account"]` | Keywords to filter |
|
||||||
|
| `consoleLogFilter.regexPatterns` | string[] | `[]` | Regex patterns for filtering |
|
||||||
|
|
||||||
|
### Proxy
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|----------|------|----------|-------------|
|
||||||
|
| `proxy.queryEngine` | boolean | `true` | Proxy query engine requests |
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|----------|------|----------|-------------|
|
||||||
|
| `webhook.discord.enabled` | boolean | `false` | Enable Discord webhook |
|
||||||
|
| `webhook.discord.url` | string | `""` | Discord webhook URL |
|
||||||
|
| `webhook.ntfy.enabled` | boolean | `false` | Enable ntfy notifications |
|
||||||
|
| `webhook.ntfy.url` | string | `""` | ntfy server URL |
|
||||||
|
| `webhook.ntfy.topic` | string | `""` | ntfy topic |
|
||||||
|
| `webhook.ntfy.token` | string | `""` | ntfy authentication token |
|
||||||
|
| `webhook.ntfy.title` | string | `"Microsoft-Rewards-Script"` | Notification title |
|
||||||
|
| `webhook.ntfy.tags` | string[] | `["bot", "notify"]` | Notification tags |
|
||||||
|
| `webhook.ntfy.priority` | number | `3` | Notification priority (1-5) |
|
||||||
|
| `webhook.webhookLogFilter.enabled` | boolean | `false` | Enable webhook log filtering |
|
||||||
|
| `webhook.webhookLogFilter.mode` | string | `"whitelist"` | Filter mode (whitelist/blacklist) |
|
||||||
|
| `webhook.webhookLogFilter.levels` | string[] | `["error"]` | Log levels to send |
|
||||||
|
| `webhook.webhookLogFilter.keywords` | string[] | `["starting account", "select number", "collected"]` | Keywords to filter |
|
||||||
|
| `webhook.webhookLogFilter.regexPatterns` | string[] | `[]` | Regex patterns for filtering |
|
||||||
|
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **NTFY** users set the `webhookLogFilter` to `enabled`, or you will receive push notifications for *all* logs.
|
||||||
|
> When enabled, only account start, 2FA codes, and account completion summaries are delivered as push notifcations.
|
||||||
|
> Customize which notifications you receive with the `keywords` options.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Account Setup
|
||||||
|
|
||||||
|
Edit `src/accounts.json`.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The file is a **flat array** of accounts, not `{ "accounts": [ ... ] }`.
|
||||||
|
> Rebuild the script after all changes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"email": "email_1",
|
||||||
|
"password": "password_1",
|
||||||
|
"totpSecret": "",
|
||||||
|
"recoveryEmail": "",
|
||||||
|
"geoLocale": "auto",
|
||||||
|
"langCode": "en",
|
||||||
|
"proxy": {
|
||||||
|
"proxyAxios": false,
|
||||||
|
"url": "",
|
||||||
|
"port": 0,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
|
"saveFingerprint": {
|
||||||
|
"mobile": false,
|
||||||
|
"desktop": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "email_2",
|
||||||
|
"password": "password_2",
|
||||||
|
"totpSecret": "",
|
||||||
|
"recoveryEmail": "",
|
||||||
|
"geoLocale": "auto",
|
||||||
|
"langCode": "en",
|
||||||
|
"proxy": {
|
||||||
|
"proxyAxios": false,
|
||||||
|
"url": "",
|
||||||
|
"port": 0,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
},
|
||||||
|
"saveFingerprint": {
|
||||||
|
"mobile": false,
|
||||||
|
"desktop": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `geoLocale` uses the default locale of your Microsoft profile. You can overwrite it here with a custom locale.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> When using 2FA login, adding your `totpSecret` will enable the script to automatically generate and enter the timed 6 digit code to login. To get your `totpSecret` in your Microsoft Security settings, click 'Manage how you sign in'. Add Authenticator app, when shown the QR code, select 'enter code manually'. Use this code in the `accounts.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Most login issues can be fixed by deleting your /sessions folder, and redeploying the script
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
15
compose.yaml
15
compose.yaml
@@ -4,11 +4,11 @@ services:
|
|||||||
container_name: microsoft-rewards-script
|
container_name: microsoft-rewards-script
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Volume mounts: Specify a location where you want to save the files on your local machine.
|
# Create and customize your accounts.json and config.json prior to deploying the container (default location: /src/)
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
|
||||||
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
|
- ./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
|
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
TZ: 'America/Toronto' # Set your timezone for proper scheduling
|
TZ: 'America/Toronto' # Set your timezone for proper scheduling
|
||||||
@@ -16,20 +16,15 @@ services:
|
|||||||
CRON_SCHEDULE: '0 7 * * *' # Customize your schedule, use crontab.guru for formatting
|
CRON_SCHEDULE: '0 7 * * *' # Customize your schedule, use crontab.guru for formatting
|
||||||
RUN_ON_START: 'true' # Runs the script immediately on container startup
|
RUN_ON_START: 'true' # Runs the script immediately on container startup
|
||||||
|
|
||||||
# Add scheduled start-time randomization (uncomment to customize or disable, default: enabled)
|
# Add a small random delay to the scheduled start time (uncomment to customize delay, or disable)
|
||||||
#MIN_SLEEP_MINUTES: "5"
|
#MIN_SLEEP_MINUTES: "5"
|
||||||
#MAX_SLEEP_MINUTES: "50"
|
#MAX_SLEEP_MINUTES: "50"
|
||||||
SKIP_RANDOM_SLEEP: 'false'
|
SKIP_RANDOM_SLEEP: 'false'
|
||||||
|
|
||||||
# Optionally set how long to wait before killing a stuck script run (prevents blocking future runs, default: 8 hours)
|
# Set a timeout for stuck script runs (default: 8h, uncomment to customize)
|
||||||
#STUCK_PROCESS_TIMEOUT_HOURS: "8"
|
#STUCK_PROCESS_TIMEOUT_HOURS: "8"
|
||||||
|
|
||||||
# Optional resource limits for the container
|
# Health check: ensures cron is running, container marked unhealthy if cron stops
|
||||||
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:
|
healthcheck:
|
||||||
test: ['CMD', 'sh', '-c', 'pgrep cron > /dev/null || exit 1']
|
test: ['CMD', 'sh', '-c', 'pgrep cron > /dev/null || exit 1']
|
||||||
interval: 60s
|
interval: 60s
|
||||||
|
|||||||
8
flake.lock
generated
8
flake.lock
generated
@@ -20,16 +20,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1749727998,
|
"lastModified": 1767654587,
|
||||||
"narHash": "sha256-mHv/yeUbmL91/TvV95p+mBVahm9mdQMJoqaTVTALaFw=",
|
"narHash": "sha256-FUB8UtsxWLWBzI7bObgQ0LEyvWL870NwUxbtqL5SK1A=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fd487183437963a59ba763c0cc4f27e3447dd6dd",
|
"rev": "c559c793525baabe46a431c6bc092ebcb5224a58",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.05",
|
"ref": "master",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/master";
|
||||||
flake-utils = {
|
flake-utils = {
|
||||||
url = "github:numtide/flake-utils";
|
url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|||||||
430
package-lock.json
generated
430
package-lock.json
generated
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "3.0.0",
|
"version": "3.0.2",
|
||||||
"license": "ISC",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"fingerprint-generator": "^2.1.77",
|
"fingerprint-generator": "^2.1.79",
|
||||||
"fingerprint-injector": "^2.1.77",
|
"fingerprint-injector": "^2.1.79",
|
||||||
"ghost-cursor-playwright-port": "^1.4.3",
|
"ghost-cursor-playwright-port": "^1.4.3",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
@@ -22,11 +22,15 @@
|
|||||||
"otpauth": "^9.4.1",
|
"otpauth": "^9.4.1",
|
||||||
"p-queue": "^9.0.1",
|
"p-queue": "^9.0.1",
|
||||||
"patchright": "^1.57.0",
|
"patchright": "^1.57.0",
|
||||||
"ts-node": "^10.9.2"
|
"semver": "^7.7.3",
|
||||||
|
"socks-proxy-agent": "^8.0.5",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ms": "^2.1.0",
|
"@types/ms": "^2.1.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/semver": "^7.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-modules-newline": "^0.0.6",
|
"eslint-plugin-modules-newline": "^0.0.6",
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
@@ -51,9 +55,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -145,9 +149,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -157,7 +161,7 @@
|
|||||||
"globals": "^14.0.0",
|
"globals": "^14.0.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.2.1",
|
"import-fresh": "^3.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.1",
|
||||||
"minimatch": "^3.1.2",
|
"minimatch": "^3.1.2",
|
||||||
"strip-json-comments": "^3.1.1"
|
"strip-json-comments": "^3.1.1"
|
||||||
},
|
},
|
||||||
@@ -179,6 +183,16 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -193,9 +207,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.39.1",
|
"version": "9.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -342,13 +356,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.56.1",
|
"version": "1.57.0",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.56.1"
|
"playwright": "1.57.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@@ -357,25 +371,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test/node_modules/playwright": {
|
|
||||||
"version": "1.56.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
|
||||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.56.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@@ -389,9 +384,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node12": {
|
"node_modules/@tsconfig/node12": {
|
||||||
@@ -440,30 +435,37 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.1",
|
"version": "24.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/semver": {
|
||||||
|
"version": "7.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
|
||||||
|
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
|
||||||
"integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
|
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.48.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/type-utils": "8.48.0",
|
"@typescript-eslint/type-utils": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.48.0",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"graphemer": "^1.4.0",
|
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -473,33 +475,23 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.48.0",
|
"@typescript-eslint/parser": "^8.51.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
|
||||||
"version": "7.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
|
||||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
|
||||||
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.48.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -515,14 +507,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
|
||||||
"integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
|
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.48.0",
|
"@typescript-eslint/tsconfig-utils": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.48.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -537,14 +529,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
|
||||||
"integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
|
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.48.0"
|
"@typescript-eslint/visitor-keys": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -555,9 +547,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
|
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -572,17 +564,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
|
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.48.0",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -597,9 +589,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
|
||||||
"integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
|
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -611,21 +603,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
|
||||||
"integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
|
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.48.0",
|
"@typescript-eslint/project-service": "8.51.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.48.0",
|
"@typescript-eslint/tsconfig-utils": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -639,16 +631,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
|
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.48.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.48.0"
|
"@typescript-eslint/typescript-estree": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -663,13 +655,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.48.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
|
||||||
"integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
|
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.48.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -698,6 +690,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -801,6 +794,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.4",
|
||||||
@@ -827,9 +821,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.31",
|
"version": "2.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||||
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
|
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
@@ -862,9 +856,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.0",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -880,12 +874,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
"electron-to-chromium": "^1.5.249",
|
"electron-to-chromium": "^1.5.263",
|
||||||
"node-releases": "^2.0.27",
|
"node-releases": "^2.0.27",
|
||||||
"update-browserslist-db": "^1.1.4"
|
"update-browserslist-db": "^1.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -917,9 +912,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001757",
|
"version": "1.0.30001762",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1207,9 +1202,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.262",
|
"version": "1.5.267",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||||
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
|
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/encoding-sniffer": {
|
"node_modules/encoding-sniffer": {
|
||||||
@@ -1305,11 +1300,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.39.1",
|
"version": "9.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -1317,7 +1313,7 @@
|
|||||||
"@eslint/config-helpers": "^0.4.2",
|
"@eslint/config-helpers": "^0.4.2",
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.2",
|
||||||
"@eslint/plugin-kit": "^0.4.1",
|
"@eslint/plugin-kit": "^0.4.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
@@ -1431,6 +1427,16 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint/node_modules/ignore": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint/node_modules/minimatch": {
|
"node_modules/eslint/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -1476,9 +1482,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1597,13 +1603,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fingerprint-generator": {
|
"node_modules/fingerprint-generator": {
|
||||||
"version": "2.1.77",
|
"version": "2.1.79",
|
||||||
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.77.tgz",
|
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.79.tgz",
|
||||||
"integrity": "sha512-wR15VUEZnwozFiSDRV+40zxlEt3ZV3JNYvLx0CSF9D9smov4pUC6MJZJnlxtDr+Ir4oppU8vn1JXApLk/Qr5Uw==",
|
"integrity": "sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"generative-bayesian-network": "^2.1.77",
|
"generative-bayesian-network": "^2.1.79",
|
||||||
"header-generator": "^2.1.77",
|
"header-generator": "^2.1.79",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1611,12 +1617,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fingerprint-injector": {
|
"node_modules/fingerprint-injector": {
|
||||||
"version": "2.1.77",
|
"version": "2.1.79",
|
||||||
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.77.tgz",
|
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.79.tgz",
|
||||||
"integrity": "sha512-R778SIyrqgWO0P+UWKzIFWUWZz13EGu6UmV7CX3vuFDbsYIL1xiH+s+/nzPSOqFdhXyLo7d8aTOjbGbRLULoQQ==",
|
"integrity": "sha512-0tKb3wCQ92ZlLLbxRfhoduCI+rIgpsUdb3Jp66TrwoMlSeVDlmoBZfkQW4FheQFau1kg0lIg4Zk4xetBubetQQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fingerprint-generator": "^2.1.77",
|
"fingerprint-generator": "^2.1.79",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1677,9 +1683,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -1716,9 +1722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/generative-bayesian-network": {
|
"node_modules/generative-bayesian-network": {
|
||||||
"version": "2.1.77",
|
"version": "2.1.79",
|
||||||
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.77.tgz",
|
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.79.tgz",
|
||||||
"integrity": "sha512-viU4CRPsmgiklR94LhvdMndaY73BkCH1pGjmOjWbLR/ZwcUd06gKF3TCcsS3npRl74o33YSInSixxm16wIukcA==",
|
"integrity": "sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
@@ -1850,13 +1856,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graphemer": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -1906,13 +1905,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/header-generator": {
|
"node_modules/header-generator": {
|
||||||
"version": "2.1.77",
|
"version": "2.1.79",
|
||||||
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.77.tgz",
|
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.79.tgz",
|
||||||
"integrity": "sha512-ggSG/mfkFMu8CO7xP591G8kp1IJCBvgXu7M8oxTjC9u914JsIzE6zIfoFsXzA+pf0utWJhUsdqU0oV/DtQ4DFQ==",
|
"integrity": "sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.21.1",
|
"browserslist": "^4.21.1",
|
||||||
"generative-bayesian-network": "^2.1.77",
|
"generative-bayesian-network": "^2.1.79",
|
||||||
"ow": "^0.28.1",
|
"ow": "^0.28.1",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
@@ -1990,9 +1989,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2026,6 +2025,15 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -2166,11 +2174,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "11.2.2",
|
"version": "11.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||||
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
|
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
@@ -2525,6 +2533,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2552,19 +2561,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.56.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
|
||||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright/node_modules/playwright-core": {
|
|
||||||
"version": "1.57.0",
|
"version": "1.57.0",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||||
@@ -2588,9 +2584,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.7.1",
|
"version": "3.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||||
"integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2669,7 +2665,6 @@
|
|||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -2701,6 +2696,44 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks-proxy-agent": {
|
||||||
|
"version": "8.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||||
|
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"socks": "^2.8.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -2744,9 +2777,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2823,6 +2856,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2847,9 +2881,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.4",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2905,6 +2939,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iconv-lite": "0.6.3"
|
"iconv-lite": "0.6.3"
|
||||||
@@ -2969,6 +3004,15 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||||
"author": "Netsky",
|
"author": "Netsky",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"main": "dist/index.js",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pre-build": "npm i && rimraf dist && npx patchright install chromium",
|
"pre-build": "npm i && rimraf dist && npx patchright install chromium",
|
||||||
@@ -18,8 +17,10 @@
|
|||||||
"create-docker": "docker build -t microsoft-rewards-script-docker .",
|
"create-docker": "docker build -t microsoft-rewards-script-docker .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"clear-sessions": "node ./scripts/clearSessions.js",
|
"clear-diagnostics": "rimraf diagnostics",
|
||||||
"clear-diagnostics": "rimraf diagnostics"
|
"clear-sessions": "node ./scripts/main/clearSessions.js",
|
||||||
|
"open-session": "node ./scripts/main/browserSession.js -email",
|
||||||
|
"open-session:dev": "node ./scripts/main/browserSession.js -dev -email ItsNetsky@protonmail.com"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Bing Rewards",
|
"Bing Rewards",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ms": "^2.1.0",
|
"@types/ms": "^2.1.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/semver": "^7.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-modules-newline": "^0.0.6",
|
"eslint-plugin-modules-newline": "^0.0.6",
|
||||||
@@ -45,8 +47,8 @@
|
|||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"fingerprint-generator": "^2.1.77",
|
"fingerprint-generator": "^2.1.79",
|
||||||
"fingerprint-injector": "^2.1.77",
|
"fingerprint-injector": "^2.1.79",
|
||||||
"ghost-cursor-playwright-port": "^1.4.3",
|
"ghost-cursor-playwright-port": "^1.4.3",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
@@ -54,6 +56,9 @@
|
|||||||
"otpauth": "^9.4.1",
|
"otpauth": "^9.4.1",
|
||||||
"p-queue": "^9.0.1",
|
"p-queue": "^9.0.1",
|
||||||
"patchright": "^1.57.0",
|
"patchright": "^1.57.0",
|
||||||
"ts-node": "^10.9.2"
|
"semver": "^7.7.3",
|
||||||
|
"socks-proxy-agent": "^8.0.5",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
|
||||||
|
|
||||||
const projectRoot = path.resolve(__dirname, '..')
|
|
||||||
|
|
||||||
const possibleConfigPaths = [
|
|
||||||
path.join(projectRoot, 'config.json'),
|
|
||||||
path.join(projectRoot, 'src', 'config.json'),
|
|
||||||
path.join(projectRoot, 'dist', 'config.json')
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log('[DEBUG] Project root:', projectRoot)
|
|
||||||
console.log('[DEBUG] Searching for config.json...')
|
|
||||||
|
|
||||||
let configPath = null
|
|
||||||
for (const p of possibleConfigPaths) {
|
|
||||||
console.log('[DEBUG] Checking:', p)
|
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
configPath = p
|
|
||||||
console.log('[DEBUG] Found config at:', p)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!configPath) {
|
|
||||||
console.error('[ERROR] config.json not found in any expected location!')
|
|
||||||
console.error('[ERROR] Searched:', possibleConfigPaths)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[INFO] Using config:', configPath)
|
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
||||||
|
|
||||||
if (!config.sessionPath) {
|
|
||||||
console.error("[ERROR] config.json missing 'sessionPath' key!")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[INFO] Session path from config:', config.sessionPath)
|
|
||||||
|
|
||||||
const configDir = path.dirname(configPath)
|
|
||||||
const possibleSessionDirs = [
|
|
||||||
path.resolve(configDir, config.sessionPath),
|
|
||||||
path.join(projectRoot, 'src/browser', config.sessionPath),
|
|
||||||
path.join(projectRoot, 'dist/browser', config.sessionPath)
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log('[DEBUG] Searching for session directory...')
|
|
||||||
|
|
||||||
let sessionDir = null
|
|
||||||
for (const p of possibleSessionDirs) {
|
|
||||||
console.log('[DEBUG] Checking:', p)
|
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
sessionDir = p
|
|
||||||
console.log('[DEBUG] Found session directory at:', p)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionDir) {
|
|
||||||
sessionDir = path.resolve(configDir, config.sessionPath)
|
|
||||||
console.log('[DEBUG] Using fallback session directory:', sessionDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedSessionDir = path.normalize(sessionDir)
|
|
||||||
const normalizedProjectRoot = path.normalize(projectRoot)
|
|
||||||
|
|
||||||
if (!normalizedSessionDir.startsWith(normalizedProjectRoot)) {
|
|
||||||
console.error('[ERROR] Session directory is outside project root!')
|
|
||||||
console.error('[ERROR] Project root:', normalizedProjectRoot)
|
|
||||||
console.error('[ERROR] Session directory:', normalizedSessionDir)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedSessionDir === normalizedProjectRoot) {
|
|
||||||
console.error('[ERROR] Session directory cannot be the project root!')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathSegments = normalizedSessionDir.split(path.sep)
|
|
||||||
if (pathSegments.length < 3) {
|
|
||||||
console.error('[ERROR] Session path is too shallow (safety check failed)!')
|
|
||||||
console.error('[ERROR] Path:', normalizedSessionDir)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(sessionDir)) {
|
|
||||||
console.log('[INFO] Removing session folder:', sessionDir)
|
|
||||||
try {
|
|
||||||
fs.rmSync(sessionDir, { recursive: true, force: true })
|
|
||||||
console.log('[SUCCESS] Session folder removed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ERROR] Failed to remove session folder:', error.message)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[INFO] Session folder does not exist:', sessionDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[INFO] Done.')
|
|
||||||
168
scripts/main/browserSession.js
Normal file
168
scripts/main/browserSession.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import { chromium } from 'patchright'
|
||||||
|
import { newInjectedContext } from 'fingerprint-injector'
|
||||||
|
import {
|
||||||
|
getDirname,
|
||||||
|
getProjectRoot,
|
||||||
|
log,
|
||||||
|
parseArgs,
|
||||||
|
validateEmail,
|
||||||
|
loadConfig,
|
||||||
|
loadAccounts,
|
||||||
|
findAccountByEmail,
|
||||||
|
getRuntimeBase,
|
||||||
|
getSessionPath,
|
||||||
|
loadCookies,
|
||||||
|
loadFingerprint,
|
||||||
|
buildProxyConfig,
|
||||||
|
setupCleanupHandlers
|
||||||
|
} from '../utils.js'
|
||||||
|
|
||||||
|
const __dirname = getDirname(import.meta.url)
|
||||||
|
const projectRoot = getProjectRoot(__dirname)
|
||||||
|
|
||||||
|
const args = parseArgs()
|
||||||
|
args.dev = args.dev || false
|
||||||
|
|
||||||
|
validateEmail(args.email)
|
||||||
|
|
||||||
|
const { data: config } = loadConfig(projectRoot, args.dev)
|
||||||
|
const { data: accounts } = loadAccounts(projectRoot, args.dev)
|
||||||
|
|
||||||
|
const account = findAccountByEmail(accounts, args.email)
|
||||||
|
if (!account) {
|
||||||
|
log('ERROR', `Account not found: ${args.email}`)
|
||||||
|
log('ERROR', 'Available accounts:')
|
||||||
|
accounts.forEach(acc => {
|
||||||
|
if (acc?.email) log('ERROR', ` - ${acc.email}`)
|
||||||
|
})
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const runtimeBase = getRuntimeBase(projectRoot, args.dev)
|
||||||
|
const sessionBase = getSessionPath(runtimeBase, config.sessionPath, args.email)
|
||||||
|
|
||||||
|
log('INFO', 'Validating session data...')
|
||||||
|
|
||||||
|
if (!fs.existsSync(sessionBase)) {
|
||||||
|
log('ERROR', `Session directory does not exist: ${sessionBase}`)
|
||||||
|
log('ERROR', 'Please ensure the session has been created for this account')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.baseURL) {
|
||||||
|
log('ERROR', 'baseURL is not set in config.json')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookies = await loadCookies(sessionBase, 'desktop')
|
||||||
|
let sessionType = 'desktop'
|
||||||
|
|
||||||
|
if (cookies.length === 0) {
|
||||||
|
log('WARN', 'No desktop session cookies found, trying mobile session...')
|
||||||
|
cookies = await loadCookies(sessionBase, 'mobile')
|
||||||
|
sessionType = 'mobile'
|
||||||
|
|
||||||
|
if (cookies.length === 0) {
|
||||||
|
log('ERROR', 'No cookies found in desktop or mobile session')
|
||||||
|
log('ERROR', `Session directory: ${sessionBase}`)
|
||||||
|
log('ERROR', 'Please ensure a valid session exists for this account')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log('INFO', `Using mobile session (${cookies.length} cookies)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMobile = sessionType === 'mobile'
|
||||||
|
const fingerprintEnabled = isMobile ? account.saveFingerprint?.mobile : account.saveFingerprint?.desktop
|
||||||
|
|
||||||
|
let fingerprint = null
|
||||||
|
if (fingerprintEnabled) {
|
||||||
|
fingerprint = await loadFingerprint(sessionBase, sessionType)
|
||||||
|
if (!fingerprint) {
|
||||||
|
log('ERROR', `Fingerprint is enabled for ${sessionType} but fingerprint file not found`)
|
||||||
|
log('ERROR', `Expected file: ${sessionBase}/session_fingerprint_${sessionType}.json`)
|
||||||
|
log('ERROR', 'Cannot start browser without fingerprint when it is explicitly enabled')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
log('INFO', `Loaded ${sessionType} fingerprint`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = buildProxyConfig(account)
|
||||||
|
|
||||||
|
if (account.proxy && account.proxy.url && (!proxy || !proxy.server)) {
|
||||||
|
log('ERROR', 'Proxy is configured in account but proxy data is invalid or incomplete')
|
||||||
|
log('ERROR', 'Account proxy config:', JSON.stringify(account.proxy, null, 2))
|
||||||
|
log('ERROR', 'Required fields: proxy.url, proxy.port')
|
||||||
|
log('ERROR', 'Cannot start browser without proxy when it is explicitly configured')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = fingerprint?.fingerprint?.navigator?.userAgent || fingerprint?.fingerprint?.userAgent || null
|
||||||
|
|
||||||
|
log('INFO', `Session: ${args.email} (${sessionType})`)
|
||||||
|
log('INFO', ` Cookies: ${cookies.length}`)
|
||||||
|
log('INFO', ` Fingerprint: ${fingerprint ? 'Yes' : 'No'}`)
|
||||||
|
log('INFO', ` User-Agent: ${userAgent || 'Default'}`)
|
||||||
|
log('INFO', ` Proxy: ${proxy ? 'Yes' : 'No'}`)
|
||||||
|
log('INFO', 'Launching browser...')
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--mute-audio',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--ignore-certificate-errors',
|
||||||
|
'--ignore-certificate-errors-spki-list',
|
||||||
|
'--ignore-ssl-errors',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-default-browser-check',
|
||||||
|
'--disable-user-media-security=true',
|
||||||
|
'--disable-blink-features=Attestation',
|
||||||
|
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
||||||
|
'--disable-save-password-bubble'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
let context
|
||||||
|
if (fingerprint) {
|
||||||
|
context = await newInjectedContext(browser, { fingerprint })
|
||||||
|
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'credentials', {
|
||||||
|
value: {
|
||||||
|
create: () => Promise.reject(new Error('WebAuthn disabled')),
|
||||||
|
get: () => Promise.reject(new Error('WebAuthn disabled'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
log('SUCCESS', 'Fingerprint injected into browser context')
|
||||||
|
} else {
|
||||||
|
context = await browser.newContext({
|
||||||
|
viewport: isMobile ? { width: 375, height: 667 } : { width: 1366, height: 768 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookies.length) {
|
||||||
|
await context.addCookies(cookies)
|
||||||
|
log('INFO', `Added ${cookies.length} cookies to context`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await context.newPage()
|
||||||
|
await page.goto(config.baseURL, { waitUntil: 'domcontentloaded' })
|
||||||
|
|
||||||
|
log('SUCCESS', 'Browser opened with session loaded')
|
||||||
|
log('INFO', `Navigated to: ${config.baseURL}`)
|
||||||
|
|
||||||
|
setupCleanupHandlers(async () => {
|
||||||
|
if (browser?.isConnected?.()) {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
67
scripts/main/clearSessions.js
Normal file
67
scripts/main/clearSessions.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import {
|
||||||
|
getDirname,
|
||||||
|
getProjectRoot,
|
||||||
|
log,
|
||||||
|
loadJsonFile,
|
||||||
|
safeRemoveDirectory
|
||||||
|
} from '../utils.js'
|
||||||
|
|
||||||
|
const __dirname = getDirname(import.meta.url)
|
||||||
|
const projectRoot = getProjectRoot(__dirname)
|
||||||
|
|
||||||
|
const possibleConfigPaths = [
|
||||||
|
path.join(projectRoot, 'config.json'),
|
||||||
|
path.join(projectRoot, 'src', 'config.json'),
|
||||||
|
path.join(projectRoot, 'dist', 'config.json')
|
||||||
|
]
|
||||||
|
|
||||||
|
log('DEBUG', 'Project root:', projectRoot)
|
||||||
|
log('DEBUG', 'Searching for config.json...')
|
||||||
|
|
||||||
|
const configResult = loadJsonFile(possibleConfigPaths, true)
|
||||||
|
const config = configResult.data
|
||||||
|
const configPath = configResult.path
|
||||||
|
|
||||||
|
log('INFO', 'Using config:', configPath)
|
||||||
|
|
||||||
|
if (!config.sessionPath) {
|
||||||
|
log('ERROR', 'Invalid config.json - missing required field: sessionPath')
|
||||||
|
log('ERROR', `Config file: ${configPath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log('INFO', 'Session path from config:', config.sessionPath)
|
||||||
|
|
||||||
|
const configDir = path.dirname(configPath)
|
||||||
|
const possibleSessionDirs = [
|
||||||
|
path.resolve(configDir, config.sessionPath),
|
||||||
|
path.join(projectRoot, 'src/browser', config.sessionPath),
|
||||||
|
path.join(projectRoot, 'dist/browser', config.sessionPath)
|
||||||
|
]
|
||||||
|
|
||||||
|
log('DEBUG', 'Searching for session directory...')
|
||||||
|
|
||||||
|
let sessionDir = null
|
||||||
|
for (const p of possibleSessionDirs) {
|
||||||
|
log('DEBUG', 'Checking:', p)
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
sessionDir = p
|
||||||
|
log('DEBUG', 'Found session directory at:', p)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionDir) {
|
||||||
|
sessionDir = path.resolve(configDir, config.sessionPath)
|
||||||
|
log('DEBUG', 'Using fallback session directory:', sessionDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = safeRemoveDirectory(sessionDir, projectRoot)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log('INFO', 'Done.')
|
||||||
269
scripts/utils.js
Normal file
269
scripts/utils.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
export function getDirname(importMetaUrl) {
|
||||||
|
const __filename = fileURLToPath(importMetaUrl)
|
||||||
|
return path.dirname(__filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectRoot(currentDir) {
|
||||||
|
let dir = currentDir
|
||||||
|
while (dir !== path.parse(dir).root) {
|
||||||
|
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
dir = path.dirname(dir)
|
||||||
|
}
|
||||||
|
throw new Error('Could not find project root (package.json not found)')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(level, ...args) {
|
||||||
|
console.log(`[${level}]`, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseArgs(argv = process.argv.slice(2)) {
|
||||||
|
const args = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i]
|
||||||
|
|
||||||
|
if (arg.startsWith('-')) {
|
||||||
|
const key = arg.substring(1)
|
||||||
|
|
||||||
|
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
||||||
|
args[key] = argv[i + 1]
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
args[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEmail(email) {
|
||||||
|
if (!email) {
|
||||||
|
log('ERROR', 'Missing -email argument')
|
||||||
|
log('ERROR', 'Usage: node script.js -email you@example.com')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof email !== 'string') {
|
||||||
|
log('ERROR', `Invalid email type: expected string, got ${typeof email}`)
|
||||||
|
log('ERROR', 'Usage: node script.js -email you@example.com')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.includes('@')) {
|
||||||
|
log('ERROR', `Invalid email format: "${email}"`)
|
||||||
|
log('ERROR', 'Email must contain "@" symbol')
|
||||||
|
log('ERROR', 'Example: you@example.com')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadJsonFile(possiblePaths, required = true) {
|
||||||
|
for (const filePath of possiblePaths) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
return { data: JSON.parse(content), path: filePath }
|
||||||
|
} catch (error) {
|
||||||
|
log('ERROR', `Failed to parse JSON file: ${filePath}`)
|
||||||
|
log('ERROR', `Parse error: ${error.message}`)
|
||||||
|
if (required) process.exit(1)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required) {
|
||||||
|
log('ERROR', 'Required file not found')
|
||||||
|
log('ERROR', 'Searched in the following locations:')
|
||||||
|
possiblePaths.forEach(p => log('ERROR', ` - ${p}`))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(projectRoot, isDev = false) {
|
||||||
|
const possiblePaths = isDev
|
||||||
|
? [path.join(projectRoot, 'src', 'config.json')]
|
||||||
|
: [
|
||||||
|
path.join(projectRoot, 'dist', 'config.json'),
|
||||||
|
path.join(projectRoot, 'config.json')
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = loadJsonFile(possiblePaths, true)
|
||||||
|
|
||||||
|
const missingFields = []
|
||||||
|
if (!result.data.baseURL) missingFields.push('baseURL')
|
||||||
|
if (!result.data.sessionPath) missingFields.push('sessionPath')
|
||||||
|
if (result.data.headless === undefined) missingFields.push('headless')
|
||||||
|
if (!result.data.workers) missingFields.push('workers')
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
log('ERROR', 'Invalid config.json - missing required fields:')
|
||||||
|
missingFields.forEach(field => log('ERROR', ` - ${field}`))
|
||||||
|
log('ERROR', `Config file: ${result.path}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadAccounts(projectRoot, isDev = false) {
|
||||||
|
const possiblePaths = isDev
|
||||||
|
? [path.join(projectRoot, 'src', 'accounts.dev.json')]
|
||||||
|
: [
|
||||||
|
path.join(projectRoot, 'dist', 'accounts.json'),
|
||||||
|
path.join(projectRoot, 'accounts.json'),
|
||||||
|
path.join(projectRoot, 'accounts.example.json')
|
||||||
|
]
|
||||||
|
|
||||||
|
return loadJsonFile(possiblePaths, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findAccountByEmail(accounts, email) {
|
||||||
|
if (!email || typeof email !== 'string') return null
|
||||||
|
return accounts.find(a => a?.email && typeof a.email === 'string' && a.email.toLowerCase() === email.toLowerCase()) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRuntimeBase(projectRoot, isDev = false) {
|
||||||
|
return path.join(projectRoot, isDev ? 'src' : 'dist')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSessionPath(runtimeBase, sessionPath, email) {
|
||||||
|
return path.join(runtimeBase, 'browser', sessionPath, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCookies(sessionBase, type = 'desktop') {
|
||||||
|
const cookiesFile = path.join(sessionBase, `session_${type}.json`)
|
||||||
|
|
||||||
|
if (!fs.existsSync(cookiesFile)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(cookiesFile, 'utf8')
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
log('WARN', `Failed to load cookies from: ${cookiesFile}`)
|
||||||
|
log('WARN', `Error: ${error.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFingerprint(sessionBase, type = 'desktop') {
|
||||||
|
const fpFile = path.join(sessionBase, `session_fingerprint_${type}.json`)
|
||||||
|
|
||||||
|
if (!fs.existsSync(fpFile)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(fpFile, 'utf8')
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
log('WARN', `Failed to load fingerprint from: ${fpFile}`)
|
||||||
|
log('WARN', `Error: ${error.message}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserAgent(fingerprint) {
|
||||||
|
if (!fingerprint) return null
|
||||||
|
return fingerprint?.fingerprint?.userAgent || fingerprint?.userAgent || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildProxyConfig(account) {
|
||||||
|
if (!account.proxy || !account.proxy.url || !account.proxy.port) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = {
|
||||||
|
server: `${account.proxy.url}:${account.proxy.port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.proxy.username && account.proxy.password) {
|
||||||
|
proxy.username = account.proxy.username
|
||||||
|
proxy.password = account.proxy.password
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupCleanupHandlers(cleanupFn) {
|
||||||
|
const cleanup = async () => {
|
||||||
|
try {
|
||||||
|
await cleanupFn()
|
||||||
|
} catch (error) {
|
||||||
|
log('ERROR', 'Cleanup failed:', error.message)
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', cleanup)
|
||||||
|
process.on('SIGTERM', cleanup)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDeletionPath(targetPath, projectRoot) {
|
||||||
|
const normalizedTarget = path.normalize(targetPath)
|
||||||
|
const normalizedRoot = path.normalize(projectRoot)
|
||||||
|
|
||||||
|
if (!normalizedTarget.startsWith(normalizedRoot)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Path is outside project root'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedTarget === normalizedRoot) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Cannot delete project root'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSegments = normalizedTarget.split(path.sep)
|
||||||
|
if (pathSegments.length < 3) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'Path is too shallow (safety check failed)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeRemoveDirectory(dirPath, projectRoot) {
|
||||||
|
const validation = validateDeletionPath(dirPath, projectRoot)
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
log('ERROR', 'Directory deletion failed - safety check:')
|
||||||
|
log('ERROR', ` Reason: ${validation.error}`)
|
||||||
|
log('ERROR', ` Target: ${dirPath}`)
|
||||||
|
log('ERROR', ` Project root: ${projectRoot}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
log('INFO', `Directory does not exist: ${dirPath}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||||
|
log('SUCCESS', `Directory removed: ${dirPath}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
log('ERROR', `Failed to remove directory: ${dirPath}`)
|
||||||
|
log('ERROR', `Error: ${error.message}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,27 +2,39 @@
|
|||||||
{
|
{
|
||||||
"email": "email_1",
|
"email": "email_1",
|
||||||
"password": "password_1",
|
"password": "password_1",
|
||||||
"totp": "",
|
"totpSecret": "",
|
||||||
|
"recoveryEmail": "",
|
||||||
"geoLocale": "auto",
|
"geoLocale": "auto",
|
||||||
|
"langCode": "en",
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"proxyAxios": true,
|
"proxyAxios": false,
|
||||||
"url": "",
|
"url": "",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"username": "",
|
"username": "",
|
||||||
"password": ""
|
"password": ""
|
||||||
|
},
|
||||||
|
"saveFingerprint": {
|
||||||
|
"mobile": false,
|
||||||
|
"desktop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": "email_2",
|
"email": "email_2",
|
||||||
"password": "password_2",
|
"password": "password_2",
|
||||||
"totp": "",
|
"totpSecret": "",
|
||||||
|
"recoveryEmail": "",
|
||||||
"geoLocale": "auto",
|
"geoLocale": "auto",
|
||||||
|
"langCode": "en",
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"proxyAxios": true,
|
"proxyAxios": false,
|
||||||
"url": "",
|
"url": "",
|
||||||
"port": 0,
|
"port": 0,
|
||||||
"username": "",
|
"username": "",
|
||||||
"password": ""
|
"password": ""
|
||||||
|
},
|
||||||
|
"saveFingerprint": {
|
||||||
|
"mobile": false,
|
||||||
|
"desktop": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import rebrowser, { BrowserContext } from 'patchright'
|
import rebrowser, { BrowserContext } from 'patchright'
|
||||||
|
|
||||||
import { newInjectedContext } from 'fingerprint-injector'
|
import { newInjectedContext } from 'fingerprint-injector'
|
||||||
import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator'
|
import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator'
|
||||||
|
|
||||||
@@ -7,7 +6,7 @@ import type { MicrosoftRewardsBot } from '../index'
|
|||||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||||
import { UserAgentManager } from './UserAgent'
|
import { UserAgentManager } from './UserAgent'
|
||||||
|
|
||||||
import type { AccountProxy } from '../interface/Account'
|
import type { Account, AccountProxy } from '../interface/Account'
|
||||||
|
|
||||||
/* Test Stuff
|
/* Test Stuff
|
||||||
https://abrahamjuliot.github.io/creepjs/
|
https://abrahamjuliot.github.io/creepjs/
|
||||||
@@ -17,28 +16,14 @@ https://pixelscan.net/
|
|||||||
https://www.browserscan.net/
|
https://www.browserscan.net/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Browser {
|
interface BrowserCreationResult {
|
||||||
private bot: MicrosoftRewardsBot
|
|
||||||
|
|
||||||
constructor(bot: MicrosoftRewardsBot) {
|
|
||||||
this.bot = bot
|
|
||||||
}
|
|
||||||
|
|
||||||
async createBrowser(
|
|
||||||
proxy: AccountProxy,
|
|
||||||
email: string
|
|
||||||
): Promise<{
|
|
||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
fingerprint: BrowserFingerprintWithHeaders
|
fingerprint: BrowserFingerprintWithHeaders
|
||||||
}> {
|
}
|
||||||
let browser: rebrowser.Browser
|
|
||||||
try {
|
class Browser {
|
||||||
browser = await rebrowser.chromium.launch({
|
private readonly bot: MicrosoftRewardsBot
|
||||||
headless: this.bot.config.headless,
|
private static readonly BROWSER_ARGS = [
|
||||||
...(proxy.url && {
|
|
||||||
proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` }
|
|
||||||
}),
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
'--mute-audio',
|
'--mute-audio',
|
||||||
'--disable-setuid-sandbox',
|
'--disable-setuid-sandbox',
|
||||||
@@ -51,29 +36,48 @@ class Browser {
|
|||||||
'--disable-blink-features=Attestation',
|
'--disable-blink-features=Attestation',
|
||||||
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
||||||
'--disable-save-password-bubble'
|
'--disable-save-password-bubble'
|
||||||
]
|
] as const
|
||||||
|
|
||||||
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
|
this.bot = bot
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBrowser(account: Account): Promise<BrowserCreationResult> {
|
||||||
|
let browser: rebrowser.Browser
|
||||||
|
try {
|
||||||
|
const proxyConfig = account.proxy.url
|
||||||
|
? {
|
||||||
|
server: this.formatProxyServer(account.proxy),
|
||||||
|
...(account.proxy.username &&
|
||||||
|
account.proxy.password && {
|
||||||
|
username: account.proxy.username,
|
||||||
|
password: account.proxy.password
|
||||||
|
})
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
browser = await rebrowser.chromium.launch({
|
||||||
|
headless: this.bot.config.headless,
|
||||||
|
...(proxyConfig && { proxy: proxyConfig }),
|
||||||
|
args: [...Browser.BROWSER_ARGS]
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.error(
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
this.bot.isMobile,
|
this.bot.logger.error(this.bot.isMobile, 'BROWSER', `Launch failed: ${errorMessage}`)
|
||||||
'BROWSER',
|
|
||||||
`Launch failed: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const sessionData = await loadSessionData(
|
const sessionData = await loadSessionData(
|
||||||
this.bot.config.sessionPath,
|
this.bot.config.sessionPath,
|
||||||
email,
|
account.email,
|
||||||
this.bot.config.saveFingerprint,
|
account.saveFingerprint,
|
||||||
this.bot.isMobile
|
this.bot.isMobile
|
||||||
)
|
)
|
||||||
|
|
||||||
const fingerprint = sessionData.fingerprint
|
const fingerprint = sessionData.fingerprint ?? (await this.generateFingerprint(this.bot.isMobile))
|
||||||
? sessionData.fingerprint
|
|
||||||
: await this.generateFingerprint(this.bot.isMobile)
|
|
||||||
|
|
||||||
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
|
const context = await newInjectedContext(browser as any, { fingerprint })
|
||||||
|
|
||||||
await context.addInitScript(() => {
|
await context.addInitScript(() => {
|
||||||
Object.defineProperty(navigator, 'credentials', {
|
Object.defineProperty(navigator, 'credentials', {
|
||||||
@@ -88,8 +92,11 @@ class Browser {
|
|||||||
|
|
||||||
await context.addCookies(sessionData.cookies)
|
await context.addCookies(sessionData.cookies)
|
||||||
|
|
||||||
if (this.bot.config.saveFingerprint) {
|
if (
|
||||||
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
|
(account.saveFingerprint.mobile && this.bot.isMobile) ||
|
||||||
|
(account.saveFingerprint.desktop && !this.bot.isMobile)
|
||||||
|
) {
|
||||||
|
await saveFingerprintData(this.bot.config.sessionPath, account.email, this.bot.isMobile, fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
@@ -97,12 +104,22 @@ class Browser {
|
|||||||
'BROWSER',
|
'BROWSER',
|
||||||
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
|
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
|
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
|
||||||
|
|
||||||
return {
|
return { context: context as unknown as BrowserContext, fingerprint }
|
||||||
context: context as unknown as BrowserContext,
|
} catch (error) {
|
||||||
fingerprint: fingerprint
|
await browser.close().catch(() => {})
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatProxyServer(proxy: AccountProxy): string {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(proxy.url)
|
||||||
|
const protocol = urlObj.protocol.replace(':', '')
|
||||||
|
return `${protocol}://${urlObj.hostname}:${proxy.port}`
|
||||||
|
} catch {
|
||||||
|
return `${proxy.url}:${proxy.port}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,27 @@ export default class BrowserFunc {
|
|||||||
*/
|
*/
|
||||||
async getDashboardData(): Promise<DashboardData> {
|
async getDashboardData(): Promise<DashboardData> {
|
||||||
try {
|
try {
|
||||||
const cookieHeader = this.bot.cookies.mobile
|
const allowedDomains = ['bing.com', 'live.com', 'microsoftonline.com'];
|
||||||
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
|
|
||||||
.join('; ')
|
const cookieHeader = [
|
||||||
|
...new Map(
|
||||||
|
this.bot.cookies.mobile
|
||||||
|
.filter(
|
||||||
|
(c: { name: string; value: string; domain?: string }) =>
|
||||||
|
typeof c.domain === 'string' &&
|
||||||
|
allowedDomains.some(d =>
|
||||||
|
c.domain && c.domain.toLowerCase().endsWith(d)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(c => [c.name, c]) // dedupe by name, keep last
|
||||||
|
).values()
|
||||||
|
]
|
||||||
|
.map(c => `${c.name}=${c.value}`)
|
||||||
|
.join('; ');
|
||||||
|
|
||||||
|
|
||||||
const request: AxiosRequestConfig = {
|
const request: AxiosRequestConfig = {
|
||||||
url: `https://rewards.bing.com/api/getuserinfo?type=1&X-Requested-With=XMLHttpRequest&_=${Date.now()}`,
|
url: 'https://rewards.bing.com/api/getuserinfo?type=1',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
...(this.bot.fingerprint?.headers ?? {}),
|
...(this.bot.fingerprint?.headers ?? {}),
|
||||||
|
|||||||
@@ -2,25 +2,31 @@ import type { Page } from 'patchright'
|
|||||||
import type { MicrosoftRewardsBot } from '../../index'
|
import type { MicrosoftRewardsBot } from '../../index'
|
||||||
import { saveSessionData } from '../../util/Load'
|
import { saveSessionData } from '../../util/Load'
|
||||||
|
|
||||||
// Methods
|
|
||||||
import { MobileAccessLogin } from './methods/MobileAccessLogin'
|
import { MobileAccessLogin } from './methods/MobileAccessLogin'
|
||||||
import { EmailLogin } from './methods/EmailLogin'
|
import { EmailLogin } from './methods/EmailLogin'
|
||||||
import { PasswordlessLogin } from './methods/PasswordlessLogin'
|
import { PasswordlessLogin } from './methods/PasswordlessLogin'
|
||||||
import { TotpLogin } from './methods/Totp2FALogin'
|
import { TotpLogin } from './methods/Totp2FALogin'
|
||||||
|
import { CodeLogin } from './methods/GetACodeLogin'
|
||||||
|
import { RecoveryLogin } from './methods/RecoveryEmailLogin'
|
||||||
|
|
||||||
|
import type { Account } from '../../interface/Account'
|
||||||
|
|
||||||
type LoginState =
|
type LoginState =
|
||||||
| 'EMAIL_INPUT'
|
| 'EMAIL_INPUT'
|
||||||
| 'PASSWORD_INPUT'
|
| 'PASSWORD_INPUT'
|
||||||
| 'SIGN_IN_ANOTHER_WAY'
|
| 'SIGN_IN_ANOTHER_WAY'
|
||||||
|
| 'SIGN_IN_ANOTHER_WAY_EMAIL'
|
||||||
| 'PASSKEY_ERROR'
|
| 'PASSKEY_ERROR'
|
||||||
| 'PASSKEY_VIDEO'
|
| 'PASSKEY_VIDEO'
|
||||||
| 'KMSI_PROMPT'
|
| 'KMSI_PROMPT'
|
||||||
| 'LOGGED_IN'
|
| 'LOGGED_IN'
|
||||||
|
| 'RECOVERY_EMAIL_INPUT'
|
||||||
| 'ACCOUNT_LOCKED'
|
| 'ACCOUNT_LOCKED'
|
||||||
| 'ERROR_ALERT'
|
| 'ERROR_ALERT'
|
||||||
| '2FA_TOTP'
|
| '2FA_TOTP'
|
||||||
| 'LOGIN_PASSWORDLESS'
|
| 'LOGIN_PASSWORDLESS'
|
||||||
| 'GET_A_CODE'
|
| 'GET_A_CODE'
|
||||||
|
| 'GET_A_CODE_2'
|
||||||
| 'UNKNOWN'
|
| 'UNKNOWN'
|
||||||
| 'CHROMEWEBDATA_ERROR'
|
| 'CHROMEWEBDATA_ERROR'
|
||||||
|
|
||||||
@@ -28,28 +34,52 @@ export class Login {
|
|||||||
emailLogin: EmailLogin
|
emailLogin: EmailLogin
|
||||||
passwordlessLogin: PasswordlessLogin
|
passwordlessLogin: PasswordlessLogin
|
||||||
totp2FALogin: TotpLogin
|
totp2FALogin: TotpLogin
|
||||||
|
codeLogin: CodeLogin
|
||||||
|
recoveryLogin: RecoveryLogin
|
||||||
|
|
||||||
|
private readonly selectors = {
|
||||||
|
primaryButton: 'button[data-testid="primaryButton"]',
|
||||||
|
secondaryButton: 'button[data-testid="secondaryButton"]',
|
||||||
|
emailIcon: '[data-testid="tile"]:has(svg path[d*="M5.25 4h13.5a3.25"])',
|
||||||
|
emailIconOld: 'img[data-testid="accessibleImg"][src*="picker_verify_email"]',
|
||||||
|
recoveryEmail: '[data-testid="proof-confirmation"]',
|
||||||
|
passwordIcon: '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])',
|
||||||
|
accountLocked: '#serviceAbuseLandingTitle',
|
||||||
|
errorAlert: 'div[role="alert"]',
|
||||||
|
passwordEntry: '[data-testid="passwordEntry"]',
|
||||||
|
emailEntry: 'input#usernameEntry',
|
||||||
|
kmsiVideo: '[data-testid="kmsiVideo"]',
|
||||||
|
passKeyVideo: '[data-testid="biometricVideo"]',
|
||||||
|
passKeyError: '[data-testid="registrationImg"]',
|
||||||
|
passwordlessCheck: '[data-testid="deviceShieldCheckmarkVideo"]',
|
||||||
|
totpInput: 'input[name="otc"]',
|
||||||
|
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
|
||||||
|
identityBanner: '[data-testid="identityBanner"]',
|
||||||
|
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
|
||||||
|
bingProfile: '#id_n',
|
||||||
|
requestToken: 'input[name="__RequestVerificationToken"]',
|
||||||
|
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
|
||||||
|
} as const
|
||||||
|
|
||||||
constructor(private bot: MicrosoftRewardsBot) {
|
constructor(private bot: MicrosoftRewardsBot) {
|
||||||
this.emailLogin = new EmailLogin(this.bot)
|
this.emailLogin = new EmailLogin(this.bot)
|
||||||
this.passwordlessLogin = new PasswordlessLogin(this.bot)
|
this.passwordlessLogin = new PasswordlessLogin(this.bot)
|
||||||
this.totp2FALogin = new TotpLogin(this.bot)
|
this.totp2FALogin = new TotpLogin(this.bot)
|
||||||
|
this.codeLogin = new CodeLogin(this.bot)
|
||||||
|
this.recoveryLogin = new RecoveryLogin(this.bot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly primaryButtonSelector = 'button[data-testid="primaryButton"]'
|
async login(page: Page, account: Account) {
|
||||||
private readonly secondaryButtonSelector = 'button[data-testid="secondaryButton"]'
|
|
||||||
|
|
||||||
async login(page: Page, email: string, password: string, totpSecret?: string) {
|
|
||||||
try {
|
try {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
||||||
|
|
||||||
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
|
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
|
||||||
await this.bot.utils.wait(2000)
|
await this.bot.utils.wait(2000)
|
||||||
await this.bot.browser.utils.reloadBadPage(page)
|
await this.bot.browser.utils.reloadBadPage(page)
|
||||||
|
|
||||||
await this.bot.browser.utils.disableFido(page)
|
await this.bot.browser.utils.disableFido(page)
|
||||||
|
|
||||||
const maxIterations = 25
|
const maxIterations = 25
|
||||||
let iteration = 0
|
let iteration = 0
|
||||||
|
|
||||||
let previousState: LoginState = 'UNKNOWN'
|
let previousState: LoginState = 'UNKNOWN'
|
||||||
let sameStateCount = 0
|
let sameStateCount = 0
|
||||||
|
|
||||||
@@ -59,7 +89,7 @@ export class Login {
|
|||||||
iteration++
|
iteration++
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `State check iteration ${iteration}/${maxIterations}`)
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `State check iteration ${iteration}/${maxIterations}`)
|
||||||
|
|
||||||
const state = await this.detectCurrentState(page)
|
const state = await this.detectCurrentState(page, account)
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Current state: ${state}`)
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Current state: ${state}`)
|
||||||
|
|
||||||
if (state !== previousState && previousState !== 'UNKNOWN') {
|
if (state !== previousState && previousState !== 'UNKNOWN') {
|
||||||
@@ -68,11 +98,16 @@ export class Login {
|
|||||||
|
|
||||||
if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') {
|
if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') {
|
||||||
sameStateCount++
|
sameStateCount++
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN',
|
||||||
|
`Same state count: ${sameStateCount}/4 for state "${state}"`
|
||||||
|
)
|
||||||
if (sameStateCount >= 4) {
|
if (sameStateCount >= 4) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN',
|
'LOGIN',
|
||||||
`Stuck in state "${state}" for 4 loops. Refreshing page...`
|
`Stuck in state "${state}" for 4 loops, refreshing page`
|
||||||
)
|
)
|
||||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||||
await this.bot.utils.wait(3000)
|
await this.bot.utils.wait(3000)
|
||||||
@@ -90,8 +125,7 @@ export class Login {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldContinue = await this.handleState(state, page, email, password, totpSecret)
|
const shouldContinue = await this.handleState(state, page, account)
|
||||||
|
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
throw new Error(`Login failed or aborted at state: ${state}`)
|
throw new Error(`Login failed or aborted at state: ${state}`)
|
||||||
}
|
}
|
||||||
@@ -103,142 +137,151 @@ export class Login {
|
|||||||
throw new Error('Login timeout: exceeded maximum iterations')
|
throw new Error('Login timeout: exceeded maximum iterations')
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.finalizeLogin(page, email)
|
await this.finalizeLogin(page, account.email)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.error(
|
this.bot.logger.error(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN',
|
'LOGIN',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
)
|
)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async detectCurrentState(page: Page): Promise<LoginState> {
|
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
|
||||||
// Make sure we settled before getting a URL
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||||
|
|
||||||
const url = new URL(page.url())
|
const url = new URL(page.url())
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-CURRENT-STATE', `Current URL: ${url}`)
|
|
||||||
|
|
||||||
if (url.hostname === 'chromewebdata') {
|
if (url.hostname === 'chromewebdata') {
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'DETECT-CURRENT-STATE', 'Detected chromewebdata error page')
|
this.bot.logger.warn(this.bot.isMobile, 'DETECT-STATE', 'Detected chromewebdata error page')
|
||||||
return 'CHROMEWEBDATA_ERROR'
|
return 'CHROMEWEBDATA_ERROR'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocked = await page
|
const isLocked = await this.checkSelector(page, this.selectors.accountLocked)
|
||||||
.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 200 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'Account locked selector found')
|
||||||
return 'ACCOUNT_LOCKED'
|
return 'ACCOUNT_LOCKED'
|
||||||
}
|
}
|
||||||
|
|
||||||
// If instantly loading rewards dash, logged in
|
if (url.hostname === 'rewards.bing.com' || url.hostname === 'account.microsoft.com') {
|
||||||
if (url.hostname === 'rewards.bing.com') {
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'On rewards/account page, assuming logged in')
|
||||||
return 'LOGGED_IN'
|
return 'LOGGED_IN'
|
||||||
}
|
}
|
||||||
|
|
||||||
// If account dash, logged in
|
const stateChecks: Array<[string, LoginState]> = [
|
||||||
if (url.hostname === 'account.microsoft.com') {
|
[this.selectors.errorAlert, 'ERROR_ALERT'],
|
||||||
return 'LOGGED_IN'
|
[this.selectors.passwordEntry, 'PASSWORD_INPUT'],
|
||||||
|
[this.selectors.emailEntry, 'EMAIL_INPUT'],
|
||||||
|
[this.selectors.recoveryEmail, 'RECOVERY_EMAIL_INPUT'],
|
||||||
|
[this.selectors.kmsiVideo, 'KMSI_PROMPT'],
|
||||||
|
[this.selectors.passKeyVideo, 'PASSKEY_VIDEO'],
|
||||||
|
[this.selectors.passKeyError, 'PASSKEY_ERROR'],
|
||||||
|
[this.selectors.passwordIcon, 'SIGN_IN_ANOTHER_WAY'],
|
||||||
|
[this.selectors.emailIcon, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||||
|
[this.selectors.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||||
|
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
|
||||||
|
[this.selectors.totpInput, '2FA_TOTP'],
|
||||||
|
[this.selectors.totpInputOld, '2FA_TOTP']
|
||||||
|
]
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
stateChecks.map(async ([sel, state]) => {
|
||||||
|
const visible = await this.checkSelector(page, sel)
|
||||||
|
return visible ? state : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleStates = results.filter((s): s is LoginState => s !== null)
|
||||||
|
if (visibleStates.length > 0) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Visible states: [${visibleStates.join(', ')}]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const check = async (selector: string, state: LoginState): Promise<LoginState | null> => {
|
const [identityBanner, primaryButton, passwordEntry] = await Promise.all([
|
||||||
return page
|
this.checkSelector(page, this.selectors.identityBanner),
|
||||||
.waitForSelector(selector, { state: 'visible', timeout: 200 })
|
this.checkSelector(page, this.selectors.primaryButton),
|
||||||
.then(visible => (visible ? state : null))
|
this.checkSelector(page, this.selectors.passwordEntry)
|
||||||
.catch(() => null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all([
|
|
||||||
check('div[role="alert"]', 'ERROR_ALERT'),
|
|
||||||
check('[data-testid="passwordEntry"]', 'PASSWORD_INPUT'),
|
|
||||||
check('input#usernameEntry', 'EMAIL_INPUT'),
|
|
||||||
check('[data-testid="kmsiVideo"]', 'KMSI_PROMPT'),
|
|
||||||
check('[data-testid="biometricVideo"]', 'PASSKEY_VIDEO'),
|
|
||||||
check('[data-testid="registrationImg"]', 'PASSKEY_ERROR'),
|
|
||||||
check('[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])', 'SIGN_IN_ANOTHER_WAY'),
|
|
||||||
check('[data-testid="deviceShieldCheckmarkVideo"]', 'LOGIN_PASSWORDLESS'),
|
|
||||||
check('input[name="otc"]', '2FA_TOTP'),
|
|
||||||
check('form[name="OneTimeCodeViewForm"]', '2FA_TOTP')
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// Get a code
|
|
||||||
const identityBanner = await page
|
|
||||||
.waitForSelector('[data-testid="identityBanner"]', { state: 'visible', timeout: 200 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
const primaryButton = await page
|
|
||||||
.waitForSelector(this.primaryButtonSelector, { state: 'visible', timeout: 200 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
const passwordEntry = await page
|
|
||||||
.waitForSelector('[data-testid="passwordEntry"]', { state: 'visible', timeout: 200 })
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
|
|
||||||
if (identityBanner && primaryButton && !passwordEntry && !results.includes('2FA_TOTP')) {
|
if (identityBanner && primaryButton && !passwordEntry && !results.includes('2FA_TOTP')) {
|
||||||
results.push('GET_A_CODE') // Lower prio
|
const codeState = account?.password ? 'GET_A_CODE' : 'GET_A_CODE_2'
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DETECT-STATE',
|
||||||
|
`Get code state detected: ${codeState} (has password: ${!!account?.password})`
|
||||||
|
)
|
||||||
|
results.push(codeState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final
|
|
||||||
let foundStates = results.filter((s): s is LoginState => s !== null)
|
let foundStates = results.filter((s): s is LoginState => s !== null)
|
||||||
|
|
||||||
if (foundStates.length === 0) return 'UNKNOWN'
|
if (foundStates.length === 0) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'No matching states found')
|
||||||
|
return 'UNKNOWN'
|
||||||
|
}
|
||||||
|
|
||||||
if (foundStates.includes('ERROR_ALERT')) {
|
if (foundStates.includes('ERROR_ALERT')) {
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DETECT-STATE',
|
||||||
|
`ERROR_ALERT found - hostname: ${url.hostname}, has 2FA: ${foundStates.includes('2FA_TOTP')}`
|
||||||
|
)
|
||||||
if (url.hostname !== 'login.live.com') {
|
if (url.hostname !== 'login.live.com') {
|
||||||
// Remove ERROR_ALERT if not on login.live.com
|
|
||||||
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
||||||
}
|
}
|
||||||
if (foundStates.includes('2FA_TOTP')) {
|
if (foundStates.includes('2FA_TOTP')) {
|
||||||
// Don't throw on TOTP if expired code is entered
|
|
||||||
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
||||||
}
|
}
|
||||||
|
|
||||||
// On login.live.com, keep it
|
|
||||||
return 'ERROR_ALERT'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT'
|
if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT'
|
||||||
if (foundStates.includes('ACCOUNT_LOCKED')) return 'ACCOUNT_LOCKED'
|
|
||||||
if (foundStates.includes('PASSKEY_VIDEO')) return 'PASSKEY_VIDEO'
|
|
||||||
if (foundStates.includes('PASSKEY_ERROR')) return 'PASSKEY_ERROR'
|
|
||||||
if (foundStates.includes('KMSI_PROMPT')) return 'KMSI_PROMPT'
|
|
||||||
if (foundStates.includes('PASSWORD_INPUT')) return 'PASSWORD_INPUT'
|
|
||||||
if (foundStates.includes('EMAIL_INPUT')) return 'EMAIL_INPUT'
|
|
||||||
if (foundStates.includes('SIGN_IN_ANOTHER_WAY')) return 'SIGN_IN_ANOTHER_WAY'
|
|
||||||
if (foundStates.includes('LOGIN_PASSWORDLESS')) return 'LOGIN_PASSWORDLESS'
|
|
||||||
if (foundStates.includes('2FA_TOTP')) return '2FA_TOTP'
|
|
||||||
|
|
||||||
const mainState = foundStates[0] as LoginState
|
|
||||||
|
|
||||||
return mainState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleState(
|
const priorities: LoginState[] = [
|
||||||
state: LoginState,
|
'ACCOUNT_LOCKED',
|
||||||
page: Page,
|
'PASSKEY_VIDEO',
|
||||||
email: string,
|
'PASSKEY_ERROR',
|
||||||
password: string,
|
'KMSI_PROMPT',
|
||||||
totpSecret?: string
|
'PASSWORD_INPUT',
|
||||||
): Promise<boolean> {
|
'EMAIL_INPUT',
|
||||||
|
'SIGN_IN_ANOTHER_WAY_EMAIL',
|
||||||
|
'SIGN_IN_ANOTHER_WAY',
|
||||||
|
'LOGIN_PASSWORDLESS',
|
||||||
|
'2FA_TOTP'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const priority of priorities) {
|
||||||
|
if (foundStates.includes(priority)) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Selected state by priority: ${priority}`)
|
||||||
|
return priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Returning first found state: ${foundStates[0]}`)
|
||||||
|
return foundStates[0] as LoginState
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkSelector(page: Page, selector: string): Promise<boolean> {
|
||||||
|
return page
|
||||||
|
.waitForSelector(selector, { state: 'visible', timeout: 200 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleState(state: LoginState, page: Page, account: Account): Promise<boolean> {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Processing state: ${state}`)
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'ACCOUNT_LOCKED': {
|
case 'ACCOUNT_LOCKED': {
|
||||||
const msg = 'This account has been locked! Remove from config and restart!'
|
const msg = 'This account has been locked! Remove from config and restart!'
|
||||||
this.bot.logger.error(this.bot.isMobile, 'CHECK-LOCKED', msg)
|
this.bot.logger.error(this.bot.isMobile, 'LOGIN', msg)
|
||||||
throw new Error(msg)
|
throw new Error(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ERROR_ALERT': {
|
case 'ERROR_ALERT': {
|
||||||
const alertEl = page.locator('div[role="alert"]')
|
const alertEl = page.locator(this.selectors.errorAlert)
|
||||||
const errorMsg = await alertEl.innerText().catch(() => 'Unknown Error')
|
const errorMsg = await alertEl.innerText().catch(() => 'Unknown Error')
|
||||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN', `Account error: ${errorMsg}`)
|
this.bot.logger.error(this.bot.isMobile, 'LOGIN', `Account error: ${errorMsg}`)
|
||||||
throw new Error(`Microsoft login error message: ${errorMsg}`)
|
throw new Error(`Microsoft login error: ${errorMsg}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'LOGGED_IN':
|
case 'LOGGED_IN':
|
||||||
@@ -246,96 +289,161 @@ export class Login {
|
|||||||
|
|
||||||
case 'EMAIL_INPUT': {
|
case 'EMAIL_INPUT': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email')
|
||||||
await this.emailLogin.enterEmail(page, email)
|
await this.emailLogin.enterEmail(page, account.email)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email entry')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'PASSWORD_INPUT': {
|
case 'PASSWORD_INPUT': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password')
|
||||||
await this.emailLogin.enterPassword(page, password)
|
await this.emailLogin.enterPassword(page, account.password)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password entry')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'GET_A_CODE': {
|
case 'GET_A_CODE': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code"')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
|
||||||
// Select sign in other way
|
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
||||||
await this.bot.browser.utils.ghostClick(page, '[data-testid="viewFooter"] span[role="button"]')
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'GET_A_CODE_2': {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling "Get a code" flow')
|
||||||
|
await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after primary button click')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
|
||||||
|
await this.codeLogin.handle(page)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SIGN_IN_ANOTHER_WAY_EMAIL': {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Send a code to email"')
|
||||||
|
|
||||||
|
const emailSelector = await Promise.race([
|
||||||
|
this.checkSelector(page, this.selectors.emailIcon).then(found =>
|
||||||
|
found ? this.selectors.emailIcon : null
|
||||||
|
),
|
||||||
|
this.checkSelector(page, this.selectors.emailIconOld).then(found =>
|
||||||
|
found ? this.selectors.emailIconOld : null
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!emailSelector) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Email icon not found')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN',
|
||||||
|
`Using ${emailSelector === this.selectors.emailIcon ? 'new' : 'old'} email icon selector`
|
||||||
|
)
|
||||||
|
await this.bot.browser.utils.ghostClick(page, emailSelector)
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email icon click')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
|
||||||
|
await this.codeLogin.handle(page)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RECOVERY_EMAIL_INPUT': {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email input detected')
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout on recovery page')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating recovery email handler')
|
||||||
|
await this.recoveryLogin.handle(page, account?.recoveryEmail)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email handler completed successfully')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'CHROMEWEBDATA_ERROR': {
|
case 'CHROMEWEBDATA_ERROR': {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'chromewebdata error detected, attempting recovery')
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN',
|
|
||||||
'chromewebdata error page detected, attempting to recover to Rewards home'
|
|
||||||
)
|
|
||||||
// Try go to Rewards dashboard
|
|
||||||
try {
|
try {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', `Navigating to ${this.bot.config.baseURL}`)
|
||||||
await page
|
await page
|
||||||
.goto(this.bot.config.baseURL, {
|
.goto(this.bot.config.baseURL, {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|
||||||
await this.bot.utils.wait(3000)
|
await this.bot.utils.wait(3000)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
// If even that fails, fall back to login.live.com
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Fallback to login.live.com')
|
||||||
this.bot.logger.warn(
|
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN',
|
|
||||||
'Failed to navigate to baseURL from chromewebdata, retrying login.live.com'
|
|
||||||
)
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.goto('https://login.live.com/', {
|
.goto('https://login.live.com/', {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|
||||||
await this.bot.utils.wait(3000)
|
await this.bot.utils.wait(3000)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case '2FA_TOTP': {
|
case '2FA_TOTP': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA required')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA authentication required')
|
||||||
await this.totp2FALogin.handle(page, totpSecret)
|
await this.totp2FALogin.handle(page, account.totpSecret)
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA handler completed successfully')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'SIGN_IN_ANOTHER_WAY': {
|
case 'SIGN_IN_ANOTHER_WAY': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Use my password"')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Use my password"')
|
||||||
const passwordOption = '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])'
|
await this.bot.browser.utils.ghostClick(page, this.selectors.passwordIcon)
|
||||||
await this.bot.browser.utils.ghostClick(page, passwordOption)
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password icon click')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password option selected')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'KMSI_PROMPT': {
|
case 'KMSI_PROMPT': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt')
|
||||||
await this.bot.browser.utils.ghostClick(page, this.primaryButtonSelector)
|
await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after KMSI acceptance')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'KMSI prompt accepted')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'PASSKEY_VIDEO':
|
case 'PASSKEY_VIDEO':
|
||||||
case 'PASSKEY_ERROR': {
|
case 'PASSKEY_ERROR': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt')
|
||||||
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
|
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after Passkey skip')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passkey prompt skipped')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'LOGIN_PASSWORDLESS': {
|
case 'LOGIN_PASSWORDLESS': {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication')
|
||||||
await this.passwordlessLogin.handle(page)
|
await this.passwordlessLogin.handle(page)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after passwordless auth')
|
||||||
|
})
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passwordless authentication completed successfully')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,12 +452,13 @@ export class Login {
|
|||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN',
|
'LOGIN',
|
||||||
`Unknown state at host:${url.hostname} path:${url.pathname}. Waiting...`
|
`Unknown state at ${url.hostname}${url.pathname}, waiting`
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Unhandled state: ${state}, continuing`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,21 +472,21 @@ export class Login {
|
|||||||
if (loginRewardsSuccess) {
|
if (loginRewardsSuccess) {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully')
|
||||||
} else {
|
} else {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not verify Rewards Dashboard, assuming login valid')
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN',
|
|
||||||
'Could not verify Rewards Dashboard. Assuming login valid anyway.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting Bing session verification')
|
||||||
await this.verifyBingSession(page)
|
await this.verifyBingSession(page)
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting rewards session verification')
|
||||||
await this.getRewardsSession(page)
|
await this.getRewardsSession(page)
|
||||||
|
|
||||||
const browser = page.context()
|
const browser = page.context()
|
||||||
const cookies = await browser.cookies()
|
const cookies = await browser.cookies()
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Retrieved ${cookies.length} cookies`)
|
||||||
await saveSessionData(this.bot.config.sessionPath, cookies, email, this.bot.isMobile)
|
await saveSessionData(this.bot.config.sessionPath, cookies, email, this.bot.isMobile)
|
||||||
|
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed! Session saved!')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed, session saved')
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyBingSession(page: Page) {
|
async verifyBingSession(page: Page) {
|
||||||
@@ -393,30 +502,34 @@ export class Login {
|
|||||||
for (let i = 0; i < loopMax; i++) {
|
for (let i = 0; i < loopMax; i++) {
|
||||||
if (page.isClosed()) break
|
if (page.isClosed()) break
|
||||||
|
|
||||||
// Rare error state
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Verification loop ${i + 1}/${loopMax}`)
|
||||||
|
|
||||||
const state = await this.detectCurrentState(page)
|
const state = await this.detectCurrentState(page)
|
||||||
if (state === 'PASSKEY_ERROR') {
|
if (state === 'PASSKEY_ERROR') {
|
||||||
this.bot.logger.debug(
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Dismissing Passkey error state')
|
||||||
this.bot.isMobile,
|
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
|
||||||
'LOGIN-BING',
|
|
||||||
'Verification landed on Passkey error state! Trying to dismiss.'
|
|
||||||
)
|
|
||||||
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const u = new URL(page.url())
|
const u = new URL(page.url())
|
||||||
const atBingHome = u.hostname === 'www.bing.com' && u.pathname === '/'
|
const atBingHome = u.hostname === 'www.bing.com' && u.pathname === '/'
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-BING',
|
||||||
|
`At Bing home: ${atBingHome} (${u.hostname}${u.pathname})`
|
||||||
|
)
|
||||||
|
|
||||||
if (atBingHome) {
|
if (atBingHome) {
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
|
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
|
||||||
|
|
||||||
const signedIn = await page
|
const signedIn = await page
|
||||||
.waitForSelector('#id_n', { timeout: 3000 })
|
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false)
|
||||||
|
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Profile element found: ${signedIn}`)
|
||||||
|
|
||||||
if (signedIn || this.bot.isMobile) {
|
if (signedIn || this.bot.isMobile) {
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session established')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session verified successfully')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,16 +537,12 @@ export class Login {
|
|||||||
await this.bot.utils.wait(1000)
|
await this.bot.utils.wait(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-BING', 'Could not verify Bing session, continuing anyway')
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN-BING',
|
|
||||||
'Could not confirm Bing session after retries; continuing'
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-BING',
|
'LOGIN-BING',
|
||||||
`Bing verification error: ${error instanceof Error ? error.message : String(error)}`
|
`Verification error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +550,7 @@ export class Login {
|
|||||||
private async getRewardsSession(page: Page) {
|
private async getRewardsSession(page: Page) {
|
||||||
const loopMax = 5
|
const loopMax = 5
|
||||||
|
|
||||||
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Fetching request token')
|
this.bot.logger.info(this.bot.isMobile, 'GET-REWARD-SESSION', 'Fetching request token')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page
|
await page
|
||||||
@@ -451,11 +560,7 @@ export class Login {
|
|||||||
for (let i = 0; i < loopMax; i++) {
|
for (let i = 0; i < loopMax; i++) {
|
||||||
if (page.isClosed()) break
|
if (page.isClosed()) break
|
||||||
|
|
||||||
this.bot.logger.debug(
|
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', `Token fetch loop ${i + 1}/${loopMax}`)
|
||||||
this.bot.isMobile,
|
|
||||||
'GET-REWARD-SESSION',
|
|
||||||
`Loop ${i + 1}/${loopMax} | URL=${page.url()}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const u = new URL(page.url())
|
const u = new URL(page.url())
|
||||||
const atRewardHome = u.hostname === 'rewards.bing.com' && u.pathname === '/'
|
const atRewardHome = u.hostname === 'rewards.bing.com' && u.pathname === '/'
|
||||||
@@ -467,23 +572,27 @@ export class Login {
|
|||||||
const $ = await this.bot.browser.utils.loadInCheerio(html)
|
const $ = await this.bot.browser.utils.loadInCheerio(html)
|
||||||
|
|
||||||
const token =
|
const token =
|
||||||
$('input[name="__RequestVerificationToken"]').attr('value') ??
|
$(this.selectors.requestToken).attr('value') ??
|
||||||
$('meta[name="__RequestVerificationToken"]').attr('content') ??
|
$(this.selectors.requestTokenMeta).attr('content') ??
|
||||||
null
|
null
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.bot.requestToken = token
|
this.bot.requestToken = token
|
||||||
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Request token has been set!')
|
this.bot.logger.info(
|
||||||
|
|
||||||
this.bot.logger.debug(
|
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'GET-REWARD-SESSION',
|
'GET-REWARD-SESSION',
|
||||||
`Token extracted: ${token.substring(0, 10)}...`
|
`Request token retrieved: ${token.substring(0, 10)}...`
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token NOT found on page')
|
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token not found on page')
|
||||||
|
} else {
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'GET-REWARD-SESSION',
|
||||||
|
`Not at reward home: ${u.hostname}${u.pathname}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.bot.utils.wait(1000)
|
await this.bot.utils.wait(1000)
|
||||||
@@ -491,19 +600,20 @@ export class Login {
|
|||||||
|
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'GET-REQUEST-TOKEN',
|
'GET-REWARD-SESSION',
|
||||||
'No RequestVerificationToken found — some activities may not work'
|
'No RequestVerificationToken found, some activities may not work'
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw this.bot.logger.error(
|
throw this.bot.logger.error(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'GET-REQUEST-TOKEN',
|
'GET-REWARD-SESSION',
|
||||||
`Reward session error: ${error instanceof Error ? error.message : String(error)}`
|
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppAccessToken(page: Page, email: string) {
|
async getAppAccessToken(page: Page, email: string) {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'GET-APP-TOKEN', 'Requesting mobile access token')
|
||||||
return await new MobileAccessLogin(this.bot, page).get(email)
|
return await new MobileAccessLogin(this.bot, page).get(email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type { Page } from 'patchright'
|
||||||
|
import type { MicrosoftRewardsBot } from '../../../index'
|
||||||
|
import { getErrorMessage, getSubtitleMessage, promptInput } from './LoginUtils'
|
||||||
|
|
||||||
|
export class CodeLogin {
|
||||||
|
private readonly textInputSelector = '[data-testid="codeInputWrapper"]'
|
||||||
|
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||||
|
private readonly maxManualSeconds = 60
|
||||||
|
private readonly maxManualAttempts = 5
|
||||||
|
|
||||||
|
constructor(private bot: MicrosoftRewardsBot) {}
|
||||||
|
|
||||||
|
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const visibleInput = await page
|
||||||
|
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (visibleInput) {
|
||||||
|
await page.keyboard.type(code, { delay: 50 })
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||||
|
if (secondairyInput) {
|
||||||
|
await page.keyboard.type(code, { delay: 50 })
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'No code input field found')
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-CODE',
|
||||||
|
`Failed to fill code input: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(page: Page): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code login authentication requested')
|
||||||
|
|
||||||
|
const emailMessage = await getSubtitleMessage(page)
|
||||||
|
if (emailMessage) {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', `Page message: "${emailMessage}"`)
|
||||||
|
} else {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'Unable to retrieve email code destination')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||||
|
const code = await promptInput({
|
||||||
|
question: `Enter the 6-digit code (waiting ${this.maxManualSeconds}s): `,
|
||||||
|
timeoutSeconds: this.maxManualSeconds,
|
||||||
|
validate: code => /^\d{6}$/.test(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!code || !/^\d{6}$/.test(code)) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-CODE',
|
||||||
|
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error('Manual code input failed or timed out')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const filled = await this.fillCode(page, code)
|
||||||
|
if (!filled) {
|
||||||
|
this.bot.logger.error(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-CODE',
|
||||||
|
`Unable to fill code input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error('Code input field not found')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(500)
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||||
|
|
||||||
|
// Check if wrong code was entered
|
||||||
|
const errorMessage = await getErrorMessage(page)
|
||||||
|
if (errorMessage) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-CODE',
|
||||||
|
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input field before retrying
|
||||||
|
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||||
|
if (inputToClear) {
|
||||||
|
await inputToClear.click()
|
||||||
|
await page.keyboard.press('Control+A')
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code authentication completed successfully')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Code input failed after ${this.maxManualAttempts} attempts`)
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.error(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-CODE',
|
||||||
|
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/browser/auth/methods/LoginUtils.ts
Normal file
66
src/browser/auth/methods/LoginUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { Page } from 'patchright'
|
||||||
|
import readline from 'readline'
|
||||||
|
|
||||||
|
export interface PromptOptions {
|
||||||
|
question: string
|
||||||
|
timeoutSeconds?: number
|
||||||
|
validate?: (input: string) => boolean
|
||||||
|
transform?: (input: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function promptInput(options: PromptOptions): Promise<string | null> {
|
||||||
|
const { question, timeoutSeconds = 60, validate, transform } = options
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
})
|
||||||
|
|
||||||
|
let resolved = false
|
||||||
|
|
||||||
|
const cleanup = (result: string | null) => {
|
||||||
|
if (resolved) return
|
||||||
|
resolved = true
|
||||||
|
clearTimeout(timer)
|
||||||
|
rl.close()
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => cleanup(null), timeoutSeconds * 1000)
|
||||||
|
|
||||||
|
rl.question(question, answer => {
|
||||||
|
let value = answer.trim()
|
||||||
|
if (transform) value = transform(value)
|
||||||
|
|
||||||
|
if (validate && !validate(value)) {
|
||||||
|
cleanup(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubtitleMessage(page: Page): Promise<string | null> {
|
||||||
|
const message = await page
|
||||||
|
.waitForSelector('[data-testid="subtitle"]', { state: 'visible', timeout: 1000 })
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
const text = await message.innerText()
|
||||||
|
return text.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getErrorMessage(page: Page): Promise<string | null> {
|
||||||
|
const errorAlert = await page
|
||||||
|
.waitForSelector('div[role="alert"]', { state: 'visible', timeout: 1000 })
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!errorAlert) return null
|
||||||
|
|
||||||
|
const text = await errorAlert.innerText()
|
||||||
|
return text.trim()
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Page } from 'patchright'
|
import type { Page } from 'patchright'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { URLSearchParams } from 'url'
|
import { URLSearchParams } from 'url'
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
|
||||||
|
|
||||||
import type { MicrosoftRewardsBot } from '../../../index'
|
import type { MicrosoftRewardsBot } from '../../../index'
|
||||||
|
|
||||||
@@ -29,25 +28,70 @@ export class MobileAccessLogin {
|
|||||||
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||||
authorizeUrl.searchParams.append('login_hint', email)
|
authorizeUrl.searchParams.append('login_hint', email)
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-APP',
|
||||||
|
`Auth URL constructed: ${authorizeUrl.origin}${authorizeUrl.pathname}`
|
||||||
|
)
|
||||||
|
|
||||||
await this.bot.browser.utils.disableFido(this.page)
|
await this.bot.browser.utils.disableFido(this.page)
|
||||||
|
|
||||||
await this.page.goto(authorizeUrl.href).catch(() => {})
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Navigating to OAuth authorize URL')
|
||||||
|
|
||||||
|
await this.page.goto(authorizeUrl.href).catch(err => {
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-APP',
|
||||||
|
`page.goto() failed: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
|
||||||
|
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
let code = ''
|
let code = ''
|
||||||
|
let lastUrl = ''
|
||||||
|
|
||||||
while (Date.now() - start < this.maxTimeout) {
|
while (Date.now() - start < this.maxTimeout) {
|
||||||
const url = new URL(this.page.url())
|
const currentUrl = this.page.url()
|
||||||
|
|
||||||
|
// Log only when URL changes (high signal, no spam)
|
||||||
|
if (currentUrl !== lastUrl) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `OAuth poll URL changed → ${currentUrl}`)
|
||||||
|
lastUrl = currentUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(currentUrl)
|
||||||
|
|
||||||
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
||||||
code = url.searchParams.get('code') || ''
|
code = url.searchParams.get('code') || ''
|
||||||
if (code) break
|
|
||||||
|
if (code) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'OAuth code detected in redirect URL')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-APP',
|
||||||
|
`Invalid URL while polling: ${String(currentUrl)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await this.bot.utils.wait(1000)
|
await this.bot.utils.wait(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'Timed out waiting for OAuth code')
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-APP',
|
||||||
|
`Timed out waiting for OAuth code after ${Math.round((Date.now() - start) / 1000)}s`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `Final page URL: ${this.page.url()}`)
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,18 +101,24 @@ export class MobileAccessLogin {
|
|||||||
data.append('code', code)
|
data.append('code', code)
|
||||||
data.append('redirect_uri', this.redirectUrl)
|
data.append('redirect_uri', this.redirectUrl)
|
||||||
|
|
||||||
const request: AxiosRequestConfig = {
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Exchanging OAuth code for access token')
|
||||||
|
|
||||||
|
const response = await this.bot.axios.request({
|
||||||
url: this.tokenUrl,
|
url: this.tokenUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
data: data.toString()
|
data: data.toString()
|
||||||
}
|
})
|
||||||
|
|
||||||
const response = await this.bot.axios.request(request)
|
|
||||||
const token = (response?.data?.access_token as string) ?? ''
|
const token = (response?.data?.access_token as string) ?? ''
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response')
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-APP',
|
||||||
|
`Token response payload: ${JSON.stringify(response?.data)}`
|
||||||
|
)
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +128,11 @@ export class MobileAccessLogin {
|
|||||||
this.bot.logger.error(
|
this.bot.logger.error(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-APP',
|
'LOGIN-APP',
|
||||||
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}`
|
`MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}`
|
||||||
)
|
)
|
||||||
return ''
|
return ''
|
||||||
} finally {
|
} finally {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Returning to base URL')
|
||||||
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
|
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,15 @@ export class PasswordlessLogin {
|
|||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-PASSWORDLESS',
|
'LOGIN-PASSWORDLESS',
|
||||||
`Please approve login and select number: ${displayedNumber}`
|
`Please approve login and select number: ${displayedNumber}`,
|
||||||
|
'yellowBright'
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-PASSWORDLESS',
|
'LOGIN-PASSWORDLESS',
|
||||||
'Please approve login on your authenticator app'
|
'Please approve login on your authenticator app',
|
||||||
|
'yellowBright'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import type { Page } from 'patchright'
|
||||||
|
import type { MicrosoftRewardsBot } from '../../../index'
|
||||||
|
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||||
|
|
||||||
|
export class RecoveryLogin {
|
||||||
|
private readonly textInputSelector = '[data-testid="proof-confirmation"]'
|
||||||
|
private readonly maxManualSeconds = 60
|
||||||
|
private readonly maxManualAttempts = 5
|
||||||
|
|
||||||
|
constructor(private bot: MicrosoftRewardsBot) {}
|
||||||
|
|
||||||
|
private async fillEmail(page: Page, email: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Attempting to fill email: ${email}`)
|
||||||
|
|
||||||
|
const visibleInput = await page
|
||||||
|
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (visibleInput) {
|
||||||
|
await page.keyboard.type(email, { delay: 50 })
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Successfully filled email input field')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Email input field not found with selector: ${this.textInputSelector}`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Failed to fill email input: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle(page: Page, recoveryEmail: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email recovery authentication flow initiated')
|
||||||
|
|
||||||
|
if (recoveryEmail) {
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Using provided recovery email: ${recoveryEmail}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filled = await this.fillEmail(page, recoveryEmail)
|
||||||
|
if (!filled) {
|
||||||
|
throw new Error('Email input field not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||||
|
await this.bot.utils.wait(500)
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||||
|
})
|
||||||
|
|
||||||
|
const errorMessage = await getErrorMessage(page)
|
||||||
|
if (errorMessage) {
|
||||||
|
throw new Error(`Email verification failed: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
'No recovery email provided, will prompt user for input'
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Starting attempt ${attempt}/${this.maxManualAttempts}`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Prompting user for email input (timeout: ${this.maxManualSeconds}s)`
|
||||||
|
)
|
||||||
|
|
||||||
|
const email = await promptInput({
|
||||||
|
question: `Recovery email (waiting ${this.maxManualSeconds}s): `,
|
||||||
|
timeoutSeconds: this.maxManualSeconds,
|
||||||
|
validate: email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`No or invalid email input received (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error('Manual email input failed: no input received')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Invalid email format received (attempt ${attempt}/${this.maxManualAttempts}) | length=${email.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error('Manual email input failed: invalid format')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Valid email received from user: ${email}`)
|
||||||
|
|
||||||
|
const filled = await this.fillEmail(page, email)
|
||||||
|
if (!filled) {
|
||||||
|
this.bot.logger.error(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Failed to fill email input field (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error('Email input field not found after maximum attempts')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||||
|
await this.bot.utils.wait(500)
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||||
|
})
|
||||||
|
|
||||||
|
const errorMessage = await getErrorMessage(page)
|
||||||
|
if (errorMessage) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-RECOVERY',
|
||||||
|
`Error from page: "${errorMessage}" (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error(`Maximum attempts reached. Last error: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Clearing input field for retry')
|
||||||
|
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||||
|
if (inputToClear) {
|
||||||
|
await inputToClear.click()
|
||||||
|
await page.keyboard.press('Control+A')
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Input field cleared')
|
||||||
|
} else {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-RECOVERY', 'Could not find input field to clear')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Email input failed after ${this.maxManualAttempts} attempts`)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
|
this.bot.logger.error(this.bot.isMobile, 'LOGIN-RECOVERY', `Fatal error: ${errorMsg}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Page } from 'patchright'
|
import type { Page } from 'patchright'
|
||||||
import * as OTPAuth from 'otpauth'
|
import * as OTPAuth from 'otpauth'
|
||||||
import readline from 'readline'
|
|
||||||
import type { MicrosoftRewardsBot } from '../../../index'
|
import type { MicrosoftRewardsBot } from '../../../index'
|
||||||
|
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||||
|
|
||||||
export class TotpLogin {
|
export class TotpLogin {
|
||||||
private readonly textInputSelector =
|
private readonly textInputSelector =
|
||||||
'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5'
|
'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5'
|
||||||
private readonly hiddenInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||||
private readonly submitButtonSelector = 'button[type="submit"]'
|
private readonly submitButtonSelector = 'button[type="submit"]'
|
||||||
private readonly maxManualSeconds = 60
|
private readonly maxManualSeconds = 60
|
||||||
private readonly maxManualAttempts = 5
|
private readonly maxManualAttempts = 5
|
||||||
@@ -17,31 +17,6 @@ export class TotpLogin {
|
|||||||
return new OTPAuth.TOTP({ secret, digits: 6 }).generate()
|
return new OTPAuth.TOTP({ secret, digits: 6 }).generate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async promptManualCode(): Promise<string | null> {
|
|
||||||
return await new Promise(resolve => {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
})
|
|
||||||
|
|
||||||
let resolved = false
|
|
||||||
|
|
||||||
const cleanup = (result: string | null) => {
|
|
||||||
if (resolved) return
|
|
||||||
resolved = true
|
|
||||||
clearTimeout(timer)
|
|
||||||
rl.close()
|
|
||||||
resolve(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(() => cleanup(null), this.maxManualSeconds * 1000)
|
|
||||||
|
|
||||||
rl.question(`Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `, answer => {
|
|
||||||
cleanup(answer.trim())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fillCode(page: Page, code: string): Promise<boolean> {
|
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const visibleInput = await page
|
const visibleInput = await page
|
||||||
@@ -50,19 +25,18 @@ export class TotpLogin {
|
|||||||
|
|
||||||
if (visibleInput) {
|
if (visibleInput) {
|
||||||
await visibleInput.fill(code)
|
await visibleInput.fill(code)
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled visible TOTP text input')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const hiddenInput = await page.$(this.hiddenInputSelector)
|
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||||
|
if (secondairyInput) {
|
||||||
if (hiddenInput) {
|
await secondairyInput.fill(code)
|
||||||
await hiddenInput.fill(code)
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled hidden TOTP input')
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found (visible or hidden)')
|
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found')
|
||||||
return false
|
return false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
@@ -83,9 +57,8 @@ export class TotpLogin {
|
|||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret')
|
||||||
|
|
||||||
const filled = await this.fillCode(page, code)
|
const filled = await this.fillCode(page, code)
|
||||||
|
|
||||||
if (!filled) {
|
if (!filled) {
|
||||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to locate or fill TOTP input field')
|
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to fill TOTP input field')
|
||||||
throw new Error('TOTP input field not found')
|
throw new Error('TOTP input field not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +66,12 @@ export class TotpLogin {
|
|||||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||||
|
|
||||||
|
const errorMessage = await getErrorMessage(page)
|
||||||
|
if (errorMessage) {
|
||||||
|
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', `TOTP failed: ${errorMessage}`)
|
||||||
|
throw new Error(`TOTP authentication failed: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,45 +79,36 @@ export class TotpLogin {
|
|||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||||
const code = await this.promptManualCode()
|
const code = await promptInput({
|
||||||
|
question: `Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `,
|
||||||
|
timeoutSeconds: this.maxManualSeconds,
|
||||||
|
validate: code => /^\d{6}$/.test(code)
|
||||||
|
})
|
||||||
|
|
||||||
if (!code || !/^\d{6}$/.test(code)) {
|
if (!code || !/^\d{6}$/.test(code)) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-TOTP',
|
'LOGIN-TOTP',
|
||||||
`Invalid or missing TOTP code (attempt ${attempt}/${this.maxManualAttempts})`
|
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (attempt === this.maxManualAttempts) {
|
if (attempt === this.maxManualAttempts) {
|
||||||
throw new Error('Manual TOTP input failed or timed out')
|
throw new Error('Manual TOTP input failed or timed out')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.info(
|
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN-TOTP',
|
|
||||||
'Retrying manual TOTP input due to invalid code'
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const filled = await this.fillCode(page, code)
|
const filled = await this.fillCode(page, code)
|
||||||
|
|
||||||
if (!filled) {
|
if (!filled) {
|
||||||
this.bot.logger.error(
|
this.bot.logger.error(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-TOTP',
|
'LOGIN-TOTP',
|
||||||
`Unable to locate or fill TOTP input field (attempt ${attempt}/${this.maxManualAttempts})`
|
`Unable to fill TOTP input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (attempt === this.maxManualAttempts) {
|
if (attempt === this.maxManualAttempts) {
|
||||||
throw new Error('TOTP input field not found')
|
throw new Error('TOTP input field not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.info(
|
|
||||||
this.bot.isMobile,
|
|
||||||
'LOGIN-TOTP',
|
|
||||||
'Retrying manual TOTP input due to fill failure'
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,16 +116,31 @@ export class TotpLogin {
|
|||||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||||
|
|
||||||
|
// Check if wrong code was entered
|
||||||
|
const errorMessage = await getErrorMessage(page)
|
||||||
|
if (errorMessage) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'LOGIN-TOTP',
|
||||||
|
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attempt === this.maxManualAttempts) {
|
||||||
|
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Manual TOTP input failed after ${this.maxManualAttempts} attempts`)
|
throw new Error(`TOTP input failed after ${this.maxManualAttempts} attempts`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.error(
|
this.bot.logger.error(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'LOGIN-TOTP',
|
'LOGIN-TOTP',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||||
)
|
)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
"headless": false,
|
"headless": false,
|
||||||
"runOnZeroPoints": false,
|
"runOnZeroPoints": false,
|
||||||
"clusters": 1,
|
"clusters": 1,
|
||||||
"errorDiagnostics": true,
|
"errorDiagnostics": false,
|
||||||
"saveFingerprint": {
|
|
||||||
"mobile": false,
|
|
||||||
"desktop": false
|
|
||||||
},
|
|
||||||
"workers": {
|
"workers": {
|
||||||
"doDailySet": true,
|
"doDailySet": true,
|
||||||
|
"doSpecialPromotions": true,
|
||||||
"doMorePromotions": true,
|
"doMorePromotions": true,
|
||||||
"doPunchCards": true,
|
"doPunchCards": true,
|
||||||
"doAppPromotions": true,
|
"doAppPromotions": true,
|
||||||
@@ -25,6 +22,12 @@
|
|||||||
"scrollRandomResults": false,
|
"scrollRandomResults": false,
|
||||||
"clickRandomResults": false,
|
"clickRandomResults": false,
|
||||||
"parallelSearching": true,
|
"parallelSearching": true,
|
||||||
|
"queryEngines": [
|
||||||
|
"google",
|
||||||
|
"wikipedia",
|
||||||
|
"reddit",
|
||||||
|
"local"
|
||||||
|
],
|
||||||
"searchResultVisitTime": "10sec",
|
"searchResultVisitTime": "10sec",
|
||||||
"searchDelay": {
|
"searchDelay": {
|
||||||
"min": "30sec",
|
"min": "30sec",
|
||||||
@@ -39,8 +42,13 @@
|
|||||||
"consoleLogFilter": {
|
"consoleLogFilter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"mode": "whitelist",
|
"mode": "whitelist",
|
||||||
"levels": ["error", "warn"],
|
"levels": [
|
||||||
"keywords": ["starting account"],
|
"error",
|
||||||
|
"warn"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"starting account"
|
||||||
|
],
|
||||||
"regexPatterns": []
|
"regexPatterns": []
|
||||||
},
|
},
|
||||||
"proxy": {
|
"proxy": {
|
||||||
@@ -57,14 +65,23 @@
|
|||||||
"topic": "",
|
"topic": "",
|
||||||
"token": "",
|
"token": "",
|
||||||
"title": "Microsoft-Rewards-Script",
|
"title": "Microsoft-Rewards-Script",
|
||||||
"tags": ["bot", "notify"],
|
"tags": [
|
||||||
|
"bot",
|
||||||
|
"notify"
|
||||||
|
],
|
||||||
"priority": 3
|
"priority": 3
|
||||||
},
|
},
|
||||||
"webhookLogFilter": {
|
"webhookLogFilter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"mode": "whitelist",
|
"mode": "whitelist",
|
||||||
"levels": ["error"],
|
"levels": [
|
||||||
"keywords": ["starting account", "select number", "collected"],
|
"error"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"starting account",
|
||||||
|
"select number",
|
||||||
|
"collected"
|
||||||
|
],
|
||||||
"regexPatterns": []
|
"regexPatterns": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ import { AppReward } from './activities/app/AppReward'
|
|||||||
import { UrlReward } from './activities/api/UrlReward'
|
import { UrlReward } from './activities/api/UrlReward'
|
||||||
import { Quiz } from './activities/api/Quiz'
|
import { Quiz } from './activities/api/Quiz'
|
||||||
import { FindClippy } from './activities/api/FindClippy'
|
import { FindClippy } from './activities/api/FindClippy'
|
||||||
|
import { DoubleSearchPoints } from './activities/api/DoubleSearchPoints'
|
||||||
|
|
||||||
// Browser
|
// Browser
|
||||||
import { SearchOnBing } from './activities/browser/SearchOnBing'
|
import { SearchOnBing } from './activities/browser/SearchOnBing'
|
||||||
import { Search } from './activities/browser/Search'
|
import { Search } from './activities/browser/Search'
|
||||||
|
|
||||||
import type { BasePromotion, DashboardData, FindClippyPromotion } from '../interface/DashboardData'
|
import type {
|
||||||
|
BasePromotion,
|
||||||
|
DashboardData,
|
||||||
|
FindClippyPromotion,
|
||||||
|
PurplePromotionalItem
|
||||||
|
} from '../interface/DashboardData'
|
||||||
import type { Promotion } from '../interface/AppDashBoardData'
|
import type { Promotion } from '../interface/AppDashBoardData'
|
||||||
|
|
||||||
export default class Activities {
|
export default class Activities {
|
||||||
@@ -68,9 +74,14 @@ export default class Activities {
|
|||||||
await quiz.doQuiz(promotion)
|
await quiz.doQuiz(promotion)
|
||||||
}
|
}
|
||||||
|
|
||||||
doFindClippy = async (promotions: FindClippyPromotion): Promise<void> => {
|
doFindClippy = async (promotion: FindClippyPromotion): Promise<void> => {
|
||||||
const urlReward = new FindClippy(this.bot)
|
const findClippy = new FindClippy(this.bot)
|
||||||
await urlReward.doFindClippy(promotions)
|
await findClippy.doFindClippy(promotion)
|
||||||
|
}
|
||||||
|
|
||||||
|
doDoubleSearchPoints = async (promotion: PurplePromotionalItem): Promise<void> => {
|
||||||
|
const doubleSearchPoints = new DoubleSearchPoints(this.bot)
|
||||||
|
await doubleSearchPoints.doDoubleSearchPoints(promotion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// App Activities
|
// App Activities
|
||||||
|
|||||||
@@ -1,22 +1,207 @@
|
|||||||
import type { AxiosRequestConfig } from 'axios'
|
import type { AxiosRequestConfig } from 'axios'
|
||||||
import type {
|
import * as fs from 'fs'
|
||||||
BingSuggestionResponse,
|
import path from 'path'
|
||||||
BingTrendingTopicsResponse,
|
import type { GoogleSearch, GoogleTrendsResponse, RedditListing, WikipediaTopResponse } from '../interface/Search'
|
||||||
GoogleSearch,
|
|
||||||
GoogleTrendsResponse
|
|
||||||
} from '../interface/Search'
|
|
||||||
import type { MicrosoftRewardsBot } from '../index'
|
import type { MicrosoftRewardsBot } from '../index'
|
||||||
|
import { QueryEngine } from '../interface/Config'
|
||||||
|
|
||||||
export class QueryCore {
|
export class QueryCore {
|
||||||
constructor(private bot: MicrosoftRewardsBot) {}
|
constructor(private bot: MicrosoftRewardsBot) {}
|
||||||
|
|
||||||
|
async queryManager(
|
||||||
|
options: {
|
||||||
|
shuffle?: boolean
|
||||||
|
sourceOrder?: QueryEngine[]
|
||||||
|
related?: boolean
|
||||||
|
langCode?: string
|
||||||
|
geoLocale?: string
|
||||||
|
} = {}
|
||||||
|
): Promise<string[]> {
|
||||||
|
const {
|
||||||
|
shuffle = false,
|
||||||
|
sourceOrder = ['google', 'wikipedia', 'reddit', 'local'],
|
||||||
|
related = true,
|
||||||
|
langCode = 'en',
|
||||||
|
geoLocale = 'US'
|
||||||
|
} = options
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`start | shuffle=${shuffle}, related=${related}, lang=${langCode}, geo=${geoLocale}, sources=${sourceOrder.join(',')}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const topicLists: string[][] = []
|
||||||
|
|
||||||
|
const sourceHandlers: Record<
|
||||||
|
'google' | 'wikipedia' | 'reddit' | 'local',
|
||||||
|
(() => Promise<string[]>) | (() => string[])
|
||||||
|
> = {
|
||||||
|
google: async () => {
|
||||||
|
const topics = await this.getGoogleTrends(geoLocale.toUpperCase()).catch(() => [])
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `google: ${topics.length}`)
|
||||||
|
return topics
|
||||||
|
},
|
||||||
|
wikipedia: async () => {
|
||||||
|
const topics = await this.getWikipediaTrending(langCode).catch(() => [])
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `wikipedia: ${topics.length}`)
|
||||||
|
return topics
|
||||||
|
},
|
||||||
|
reddit: async () => {
|
||||||
|
const topics = await this.getRedditTopics().catch(() => [])
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `reddit: ${topics.length}`)
|
||||||
|
return topics
|
||||||
|
},
|
||||||
|
local: () => {
|
||||||
|
const topics = this.getLocalQueryList()
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `local: ${topics.length}`)
|
||||||
|
return topics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of sourceOrder) {
|
||||||
|
const handler = sourceHandlers[source]
|
||||||
|
if (!handler) continue
|
||||||
|
|
||||||
|
const topics = await Promise.resolve(handler())
|
||||||
|
if (topics.length) topicLists.push(topics)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`sources combined | rawTotal=${topicLists.flat().length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseTopics = this.normalizeAndDedupe(topicLists.flat())
|
||||||
|
|
||||||
|
if (!baseTopics.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'No base topics found (all sources empty)')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`baseTopics dedupe | before=${topicLists.flat().length} | after=${baseTopics.length}`
|
||||||
|
)
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `baseTopics: ${baseTopics.length}`)
|
||||||
|
|
||||||
|
const clusters = related ? await this.buildRelatedClusters(baseTopics, langCode) : baseTopics.map(t => [t])
|
||||||
|
|
||||||
|
this.bot.utils.shuffleArray(clusters)
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'clusters shuffled')
|
||||||
|
|
||||||
|
let finalQueries = clusters.flat()
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`clusters flattened | total=${finalQueries.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do not cluster searches and shuffle
|
||||||
|
if (shuffle) {
|
||||||
|
this.bot.utils.shuffleArray(finalQueries)
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries shuffled')
|
||||||
|
}
|
||||||
|
|
||||||
|
finalQueries = this.normalizeAndDedupe(finalQueries)
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`finalQueries dedupe | after=${finalQueries.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!finalQueries.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries deduped to 0')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `final queries: ${finalQueries.length}`)
|
||||||
|
|
||||||
|
return finalQueries
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`error: ${error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)}`
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildRelatedClusters(baseTopics: string[], langCode: string): Promise<string[][]> {
|
||||||
|
const clusters: string[][] = []
|
||||||
|
|
||||||
|
const LIMIT = 50
|
||||||
|
const head = baseTopics.slice(0, LIMIT)
|
||||||
|
const tail = baseTopics.slice(LIMIT)
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`related enabled | baseTopics=${baseTopics.length} | expand=${head.length} | passthrough=${tail.length} | lang=${langCode}`
|
||||||
|
)
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`bing expansion enabled | limit=${LIMIT} | totalCalls=${head.length * 2}`
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const topic of head) {
|
||||||
|
const suggestions = await this.getBingSuggestions(topic, langCode).catch(() => [])
|
||||||
|
const relatedTerms = await this.getBingRelatedTerms(topic).catch(() => [])
|
||||||
|
|
||||||
|
const usedSuggestions = suggestions.slice(0, 6)
|
||||||
|
const usedRelated = relatedTerms.slice(0, 3)
|
||||||
|
|
||||||
|
const cluster = this.normalizeAndDedupe([topic, ...usedSuggestions, ...usedRelated])
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'QUERY-MANAGER',
|
||||||
|
`cluster expanded | topic="${topic}" | suggestions=${suggestions.length}->${usedSuggestions.length} | related=${relatedTerms.length}->${usedRelated.length} | clusterSize=${cluster.length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
clusters.push(cluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tail.length) {
|
||||||
|
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `cluster passthrough | topics=${tail.length}`)
|
||||||
|
|
||||||
|
for (const topic of tail) {
|
||||||
|
clusters.push([topic])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeAndDedupe(queries: string[]): string[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const out: string[] = []
|
||||||
|
|
||||||
|
for (const q of queries) {
|
||||||
|
if (!q) continue
|
||||||
|
const trimmed = q.trim()
|
||||||
|
if (!trimmed) continue
|
||||||
|
|
||||||
|
const norm = trimmed.replace(/\s+/g, ' ').toLowerCase()
|
||||||
|
if (seen.has(norm)) continue
|
||||||
|
|
||||||
|
seen.add(norm)
|
||||||
|
out.push(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
async getGoogleTrends(geoLocale: string): Promise<string[]> {
|
async getGoogleTrends(geoLocale: string): Promise<string[]> {
|
||||||
const queryTerms: GoogleSearch[] = []
|
const queryTerms: GoogleSearch[] = []
|
||||||
this.bot.logger.info(
|
|
||||||
this.bot.isMobile,
|
|
||||||
'SEARCH-GOOGLE-TRENDS',
|
|
||||||
`Generating search queries, can take a while! | GeoLocale: ${geoLocale}`
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request: AxiosRequestConfig = {
|
const request: AxiosRequestConfig = {
|
||||||
@@ -29,163 +214,287 @@ export class QueryCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
const rawData = response.data
|
const trendsData = this.extractJsonFromResponse(response.data)
|
||||||
|
|
||||||
const trendsData = this.extractJsonFromResponse(rawData)
|
|
||||||
if (!trendsData) {
|
if (!trendsData) {
|
||||||
throw this.bot.logger.error(
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
|
||||||
this.bot.isMobile,
|
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No trendsData parsed from response')
|
||||||
'SEARCH-GOOGLE-TRENDS',
|
return []
|
||||||
'Failed to parse Google Trends response'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
|
const mapped = trendsData.map(q => [q[0], q[9]!.slice(1)])
|
||||||
if (mappedTrendsData.length < 90) {
|
|
||||||
this.bot.logger.warn(
|
if (mapped.length < 90 && geoLocale !== 'US') {
|
||||||
this.bot.isMobile,
|
|
||||||
'SEARCH-GOOGLE-TRENDS',
|
|
||||||
'Insufficient search queries, falling back to US'
|
|
||||||
)
|
|
||||||
return this.getGoogleTrends('US')
|
return this.getGoogleTrends('US')
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [topic, relatedQueries] of mappedTrendsData) {
|
for (const [topic, related] of mapped) {
|
||||||
queryTerms.push({
|
queryTerms.push({
|
||||||
topic: topic as string,
|
topic: topic as string,
|
||||||
related: relatedQueries as string[]
|
related: related as string[]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.bot.logger.error(
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'SEARCH-GOOGLE-TRENDS',
|
'SEARCH-GOOGLE-TRENDS',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`request failed: ${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const queries = queryTerms.flatMap(x => [x.topic, ...x.related])
|
return queryTerms.flatMap(x => [x.topic, ...x.related])
|
||||||
|
|
||||||
return queries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
|
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
|
||||||
const lines = text.split('\n')
|
for (const line of text.split('\n')) {
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim()
|
const trimmed = line.trim()
|
||||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
if (!trimmed.startsWith('[')) continue
|
||||||
try {
|
try {
|
||||||
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
|
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
|
||||||
} catch {
|
} catch {}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBingSuggestions(query: string = '', langCode: string = 'en'): Promise<string[]> {
|
async getBingSuggestions(query = '', langCode = 'en'): Promise<string[]> {
|
||||||
this.bot.logger.info(
|
|
||||||
this.bot.isMobile,
|
|
||||||
'SEARCH-BING-SUGGESTIONS',
|
|
||||||
`Generating bing suggestions! | LangCode: ${langCode}`
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request: AxiosRequestConfig = {
|
const request: AxiosRequestConfig = {
|
||||||
url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(query)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`,
|
url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(
|
||||||
|
query
|
||||||
|
)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
...(this.bot.fingerprint?.headers ?? {}),
|
||||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
const rawData: BingSuggestionResponse = response.data
|
const suggestions =
|
||||||
|
response.data.suggestionGroups?.[0]?.searchSuggestions?.map((x: { query: any }) => x.query) ?? []
|
||||||
|
|
||||||
const searchSuggestions = rawData.suggestionGroups[0]?.searchSuggestions
|
if (!suggestions.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
|
||||||
if (!searchSuggestions?.length) {
|
this.bot.logger.debug(
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'API returned no results')
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchSuggestions.map(x => x.query)
|
|
||||||
} catch (error) {
|
|
||||||
this.bot.logger.error(
|
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'SEARCH-GOOGLE-TRENDS',
|
'SEARCH-BING-SUGGESTIONS',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`empty suggestions | query="${query}" | lang=${langCode}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-BING-SUGGESTIONS',
|
||||||
|
`request failed | query="${query}" | lang=${langCode} | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getBingRelatedTerms(term: string): Promise<string[]> {
|
async getBingRelatedTerms(query: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const request = {
|
const request: AxiosRequestConfig = {
|
||||||
url: `https://api.bing.com/osjson.aspx?query=${term}`,
|
url: `https://api.bing.com/osjson.aspx?query=${encodeURIComponent(query)}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
...(this.bot.fingerprint?.headers ?? {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
const rawData = response.data
|
const related = response.data?.[1]
|
||||||
|
const out = Array.isArray(related) ? related : []
|
||||||
|
|
||||||
const relatedTerms = rawData[1]
|
if (!out.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
|
||||||
if (!relatedTerms?.length) {
|
this.bot.logger.debug(
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'API returned no results')
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return relatedTerms
|
|
||||||
} catch (error) {
|
|
||||||
this.bot.logger.error(
|
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'SEARCH-BING-RELATED',
|
'SEARCH-BING-RELATED',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`empty related terms | query="${query}"`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-BING-RELATED',
|
||||||
|
`request failed | query="${query}" | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getBingTendingTopics(langCode: string = 'en'): Promise<string[]> {
|
async getBingTrendingTopics(langCode = 'en'): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const request = {
|
const request: AxiosRequestConfig = {
|
||||||
url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`,
|
url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
Authorization: `Bearer ${this.bot.accessToken}`,
|
||||||
|
'User-Agent':
|
||||||
|
'Bing/32.5.431027001 (com.microsoft.bing; build:431027001; iOS 17.6.1) Alamofire/5.10.2',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Rewards-Country': this.bot.userData.geoLocale,
|
||||||
|
'X-Rewards-Language': 'en',
|
||||||
|
'X-Rewards-ismobile': 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
const rawData: BingTrendingTopicsResponse = response.data
|
const topics =
|
||||||
|
response.data.value?.map(
|
||||||
|
(x: { query: { text: string }; name: string }) => x.query?.text?.trim() || x.name.trim()
|
||||||
|
) ?? []
|
||||||
|
|
||||||
const trendingTopics = rawData.value
|
if (!topics.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
|
||||||
if (!trendingTopics?.length) {
|
this.bot.logger.debug(
|
||||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'API returned no results')
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const queries = trendingTopics.map(x => x.query?.text?.trim() || x.name.trim())
|
|
||||||
|
|
||||||
return queries
|
|
||||||
} catch (error) {
|
|
||||||
this.bot.logger.error(
|
|
||||||
this.bot.isMobile,
|
this.bot.isMobile,
|
||||||
'SEARCH-BING-TRENDING',
|
'SEARCH-BING-TRENDING',
|
||||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
`empty trending topics | lang=${langCode}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return topics
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-BING-TRENDING',
|
||||||
|
`request failed | lang=${langCode} | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWikipediaTrending(langCode = 'en'): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const date = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||||
|
const yyyy = date.getUTCFullYear()
|
||||||
|
const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(date.getUTCDate()).padStart(2, '0')
|
||||||
|
|
||||||
|
const request: AxiosRequestConfig = {
|
||||||
|
url: `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${langCode}.wikipedia/all-access/${yyyy}/${mm}/${dd}`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(this.bot.fingerprint?.headers ?? {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
|
const articles = (response.data as WikipediaTopResponse).items?.[0]?.articles ?? []
|
||||||
|
|
||||||
|
const out = articles.slice(0, 50).map(a => a.article.replace(/_/g, ' '))
|
||||||
|
|
||||||
|
if (!out.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-WIKIPEDIA-TRENDING',
|
||||||
|
`empty wikipedia top | lang=${langCode}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-WIKIPEDIA-TRENDING',
|
||||||
|
`request failed | lang=${langCode} | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRedditTopics(subreddit = 'popular'): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const safe = subreddit.replace(/[^a-zA-Z0-9_+]/g, '')
|
||||||
|
const request: AxiosRequestConfig = {
|
||||||
|
url: `https://www.reddit.com/r/${safe}.json?limit=50`,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...(this.bot.fingerprint?.headers ?? {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||||
|
const posts = (response.data as RedditListing).data?.children ?? []
|
||||||
|
|
||||||
|
const out = posts.filter(p => !p.data.over_18).map(p => p.data.title)
|
||||||
|
|
||||||
|
if (!out.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT-TRENDING', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-REDDIT-TRENDING',
|
||||||
|
`empty reddit listing | subreddit=${safe}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-REDDIT',
|
||||||
|
`request failed | subreddit=${subreddit} | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalQueryList(): string[] {
|
||||||
|
try {
|
||||||
|
const file = path.join(__dirname, './search-queries.json')
|
||||||
|
const queries = JSON.parse(fs.readFileSync(file, 'utf8')) as string[]
|
||||||
|
const out = Array.isArray(queries) ? queries : []
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-LOCAL-QUERY-LIST',
|
||||||
|
'local queries loaded | file=search-queries.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!out.length) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-LOCAL-QUERY-LIST',
|
||||||
|
'search-queries.json parsed but empty or invalid'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SEARCH-LOCAL-QUERY-LIST',
|
||||||
|
`read/parse failed | error=${
|
||||||
|
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class SearchManager {
|
|||||||
|
|
||||||
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Closing mobile session')
|
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Closing mobile session')
|
||||||
try {
|
try {
|
||||||
await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
await executionContext.run({ isMobile: true, account }, async () => {
|
||||||
await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail)
|
await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail)
|
||||||
})
|
})
|
||||||
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed')
|
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed')
|
||||||
@@ -368,7 +368,7 @@ export class SearchManager {
|
|||||||
`Init | account=${accountEmail} | proxy=${account.proxy ?? 'none'}`
|
`Init | account=${accountEmail} | proxy=${account.proxy ?? 'none'}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const session = await this.bot['browserFactory'].createBrowser(account.proxy, accountEmail)
|
const session = await this.bot['browserFactory'].createBrowser(account)
|
||||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Browser created, new page')
|
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Browser created, new page')
|
||||||
|
|
||||||
this.bot.mainDesktopPage = await session.context.newPage()
|
this.bot.mainDesktopPage = await session.context.newPage()
|
||||||
@@ -377,7 +377,7 @@ export class SearchManager {
|
|||||||
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login start')
|
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login start')
|
||||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Calling login handler')
|
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Calling login handler')
|
||||||
|
|
||||||
await this.bot['login'].login(this.bot.mainDesktopPage, accountEmail, account.password, account.totp)
|
await this.bot['login'].login(this.bot.mainDesktopPage, account)
|
||||||
|
|
||||||
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login passed, verifying')
|
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login passed, verifying')
|
||||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession')
|
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession')
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { Page } from 'patchright'
|
import type { Page } from 'patchright'
|
||||||
import type { MicrosoftRewardsBot } from '../index'
|
import type { MicrosoftRewardsBot } from '../index'
|
||||||
import type { DashboardData, PunchCard, BasePromotion, FindClippyPromotion } from '../interface/DashboardData'
|
import type {
|
||||||
|
DashboardData,
|
||||||
|
PunchCard,
|
||||||
|
BasePromotion,
|
||||||
|
FindClippyPromotion,
|
||||||
|
PurplePromotionalItem
|
||||||
|
} from '../interface/DashboardData'
|
||||||
import type { AppDashboardData } from '../interface/AppDashBoardData'
|
import type { AppDashboardData } from '../interface/AppDashBoardData'
|
||||||
|
|
||||||
export class Workers {
|
export class Workers {
|
||||||
@@ -38,13 +44,14 @@ export class Workers {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const activitiesUncompleted: BasePromotion[] =
|
const activitiesUncompleted: BasePromotion[] =
|
||||||
morePromotions?.filter(
|
morePromotions?.filter(x => {
|
||||||
x =>
|
if (x.complete) return false
|
||||||
!x.complete &&
|
if (x.pointProgressMax <= 0) return false
|
||||||
x.pointProgressMax > 0 &&
|
if (x.exclusiveLockedFeatureStatus === 'locked') return false
|
||||||
x.exclusiveLockedFeatureStatus !== 'locked' &&
|
if (!x.promotionType) return false
|
||||||
x.promotionType
|
|
||||||
) ?? []
|
return true
|
||||||
|
}) ?? []
|
||||||
|
|
||||||
if (!activitiesUncompleted.length) {
|
if (!activitiesUncompleted.length) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
@@ -67,13 +74,14 @@ export class Workers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async doAppPromotions(data: AppDashboardData) {
|
public async doAppPromotions(data: AppDashboardData) {
|
||||||
const appRewards = data.response.promotions.filter(
|
const appRewards = data.response.promotions.filter(x => {
|
||||||
x =>
|
if (x.attributes['complete']?.toLowerCase() !== 'false') return false
|
||||||
x.attributes['complete']?.toLowerCase() === 'false' &&
|
if (!x.attributes['offerid']) return false
|
||||||
x.attributes['offerid'] &&
|
if (!x.attributes['type']) return false
|
||||||
x.attributes['type'] &&
|
if (x.attributes['type'] !== 'sapphire') return false
|
||||||
x.attributes['type'] === 'sapphire'
|
|
||||||
)
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
if (!appRewards.length) {
|
if (!appRewards.length) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
@@ -93,6 +101,77 @@ export class Workers {
|
|||||||
this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', 'All "App Promotions" items have been completed')
|
this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', 'All "App Promotions" items have been completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async doSpecialPromotions(data: DashboardData) {
|
||||||
|
const specialPromotions: PurplePromotionalItem[] = [
|
||||||
|
...new Map(
|
||||||
|
[...(data.promotionalItems ?? [])]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(p => [p.offerId, p as PurplePromotionalItem] as const)
|
||||||
|
).values()
|
||||||
|
]
|
||||||
|
|
||||||
|
const supportedPromotions = ['ww_banner_optin_2x']
|
||||||
|
|
||||||
|
const specialPromotionsUncompleted: PurplePromotionalItem[] =
|
||||||
|
specialPromotions?.filter(x => {
|
||||||
|
if (x.complete) return false
|
||||||
|
if (x.exclusiveLockedFeatureStatus === 'locked') return false
|
||||||
|
if (!x.promotionType) return false
|
||||||
|
|
||||||
|
const offerId = (x.offerId ?? '').toLowerCase()
|
||||||
|
return supportedPromotions.some(s => offerId.includes(s))
|
||||||
|
}) ?? []
|
||||||
|
|
||||||
|
for (const activity of specialPromotionsUncompleted) {
|
||||||
|
try {
|
||||||
|
const type = activity.promotionType?.toLowerCase() ?? ''
|
||||||
|
const name = activity.name?.toLowerCase() ?? ''
|
||||||
|
const offerId = (activity as PurplePromotionalItem).offerId
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SPECIAL-ACTIVITY',
|
||||||
|
`Processing activity | title="${activity.title}" | offerId=${offerId} | type=${type}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
// UrlReward
|
||||||
|
case 'urlreward': {
|
||||||
|
// Special "Double Search Points" activation
|
||||||
|
if (name.includes('ww_banner_optin_2x')) {
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'ACTIVITY',
|
||||||
|
`Found activity type "Double Search Points" | title="${activity.title}" | offerId=${offerId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.bot.activities.doDoubleSearchPoints(activity)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsupported types
|
||||||
|
default: {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SPECIAL-ACTIVITY',
|
||||||
|
`Skipped activity "${activity.title}" | offerId=${offerId} | Reason: Unsupported type "${activity.promotionType}"`
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.error(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'SPECIAL-ACTIVITY',
|
||||||
|
`Error while solving activity "${activity.title}" | message=${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.info(this.bot.isMobile, 'SPECIAL-ACTIVITY', 'All "Special Activites" items have been completed')
|
||||||
|
}
|
||||||
|
|
||||||
private async solveActivities(activities: BasePromotion[], page: Page, punchCard?: PunchCard) {
|
private async solveActivities(activities: BasePromotion[], page: Page, punchCard?: PunchCard) {
|
||||||
for (const activity of activities) {
|
for (const activity of activities) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
124
src/functions/activities/api/DoubleSearchPoints.ts
Normal file
124
src/functions/activities/api/DoubleSearchPoints.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import type { AxiosRequestConfig } from 'axios'
|
||||||
|
import { Workers } from '../../Workers'
|
||||||
|
import { PromotionalItem } from '../../../interface/DashboardData'
|
||||||
|
|
||||||
|
export class DoubleSearchPoints extends Workers {
|
||||||
|
private cookieHeader: string = ''
|
||||||
|
|
||||||
|
private fingerprintHeader: { [x: string]: string } = {}
|
||||||
|
|
||||||
|
public async doDoubleSearchPoints(promotion: PromotionalItem) {
|
||||||
|
const offerId = promotion.offerId
|
||||||
|
const activityType = promotion.activityType
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.bot.requestToken) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
'Skipping: Request token not available, this activity requires it!'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cookieHeader = (this.bot.isMobile ? this.bot.cookies.mobile : this.bot.cookies.desktop)
|
||||||
|
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
|
||||||
|
.join('; ')
|
||||||
|
|
||||||
|
const fingerprintHeaders = { ...this.bot.fingerprint.headers }
|
||||||
|
delete fingerprintHeaders['Cookie']
|
||||||
|
delete fingerprintHeaders['cookie']
|
||||||
|
this.fingerprintHeader = fingerprintHeaders
|
||||||
|
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Starting Double Search Points | offerId=${offerId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Prepared headers | cookieLength=${this.cookieHeader.length} | fingerprintHeaderKeys=${Object.keys(this.fingerprintHeader).length}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const formData = new URLSearchParams({
|
||||||
|
id: offerId,
|
||||||
|
hash: promotion.hash,
|
||||||
|
timeZone: '60',
|
||||||
|
activityAmount: '1',
|
||||||
|
dbs: '0',
|
||||||
|
form: '',
|
||||||
|
type: activityType,
|
||||||
|
__RequestVerificationToken: this.bot.requestToken
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Prepared Double Search Points form data | offerId=${offerId} | hash=${promotion.hash} | timeZone=60 | activityAmount=1 | type=${activityType}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const request: AxiosRequestConfig = {
|
||||||
|
url: 'https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(this.bot.fingerprint?.headers ?? {}),
|
||||||
|
Cookie: this.cookieHeader,
|
||||||
|
Referer: 'https://rewards.bing.com/',
|
||||||
|
Origin: 'https://rewards.bing.com'
|
||||||
|
},
|
||||||
|
data: formData
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Sending Double Search Points request | offerId=${offerId} | url=${request.url}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await this.bot.axios.request(request)
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Received Double Search Points response | offerId=${offerId} | status=${response.status}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await this.bot.browser.func.getDashboardData()
|
||||||
|
const promotionalItem = data.promotionalItems.find(item =>
|
||||||
|
item.name.toLowerCase().includes('ww_banner_optin_2x')
|
||||||
|
)
|
||||||
|
|
||||||
|
// If OK, should no longer be presernt in promotionalItems
|
||||||
|
if (promotionalItem) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Unable to find or activate Double Search Points | offerId=${offerId} | status=${response.status}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.bot.logger.info(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Activated Double Search Points | offerId=${offerId} | status=${response.status}`,
|
||||||
|
'green'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.logger.debug(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Waiting after Double Search Points | offerId=${offerId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.logger.error(
|
||||||
|
this.bot.isMobile,
|
||||||
|
'DOUBLE-SEARCH-POINTS',
|
||||||
|
`Error in doDoubleSearchPoints | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,38 +33,34 @@ export class Search extends Workers {
|
|||||||
`Search points remaining | Edge=${missingPoints.edgePoints} | Desktop=${missingPoints.desktopPoints} | Mobile=${missingPoints.mobilePoints}`
|
`Search points remaining | Edge=${missingPoints.edgePoints} | Desktop=${missingPoints.desktopPoints} | Mobile=${missingPoints.mobilePoints}`
|
||||||
)
|
)
|
||||||
|
|
||||||
let queries: string[] = []
|
|
||||||
|
|
||||||
const queryCore = new QueryCore(this.bot)
|
const queryCore = new QueryCore(this.bot)
|
||||||
|
const locale = (this.bot.userData.geoLocale ?? 'US').toUpperCase()
|
||||||
|
const langCode = (this.bot.userData.langCode ?? 'en').toLowerCase()
|
||||||
|
|
||||||
const locale = this.bot.userData.geoLocale.toUpperCase()
|
this.bot.logger.debug(
|
||||||
|
isMobile,
|
||||||
|
'SEARCH-BING',
|
||||||
|
`Resolving search queries via QueryCore | locale=${locale} | lang=${langCode} | related=true`
|
||||||
|
)
|
||||||
|
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Resolving search queries | locale=${locale}`)
|
let queries = await queryCore.queryManager({
|
||||||
|
shuffle: true,
|
||||||
|
related: true,
|
||||||
|
langCode,
|
||||||
|
geoLocale: locale,
|
||||||
|
sourceOrder: ['google', 'wikipedia', 'reddit', 'local']
|
||||||
|
})
|
||||||
|
|
||||||
// Set Google search queries
|
queries = [...new Set(queries.map(q => q.trim()).filter(Boolean))]
|
||||||
queries = await queryCore.getGoogleTrends(locale)
|
|
||||||
|
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Fetched base queries | count=${queries.length}`)
|
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool ready | count=${queries.length}`)
|
||||||
|
|
||||||
// Deduplicate queries
|
|
||||||
queries = [...new Set(queries)]
|
|
||||||
|
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Deduplicated queries | count=${queries.length}`)
|
|
||||||
|
|
||||||
// Shuffle
|
|
||||||
queries = this.bot.utils.shuffleArray(queries)
|
|
||||||
|
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Shuffled queries | count=${queries.length}`)
|
|
||||||
|
|
||||||
// Go to bing
|
// Go to bing
|
||||||
const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome
|
const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`)
|
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`)
|
||||||
|
|
||||||
await page.goto(targetUrl)
|
await page.goto(targetUrl)
|
||||||
|
|
||||||
// Wait until page loaded
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||||
|
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||||
|
|
||||||
let stagnantLoop = 0
|
let stagnantLoop = 0
|
||||||
@@ -77,7 +73,6 @@ export class Search extends Workers {
|
|||||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||||
|
|
||||||
// Points gained for THIS query only
|
|
||||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||||
const gainedPoints = Math.max(0, rawGained)
|
const gainedPoints = Math.max(0, rawGained)
|
||||||
|
|
||||||
@@ -91,12 +86,10 @@ export class Search extends Workers {
|
|||||||
} else {
|
} else {
|
||||||
stagnantLoop = 0
|
stagnantLoop = 0
|
||||||
|
|
||||||
// Update global user data
|
|
||||||
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
||||||
this.bot.userData.currentPoints = newBalance
|
this.bot.userData.currentPoints = newBalance
|
||||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||||
|
|
||||||
// Track for return value
|
|
||||||
totalGainedPoints += gainedPoints
|
totalGainedPoints += gainedPoints
|
||||||
|
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
@@ -107,10 +100,8 @@ export class Search extends Workers {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update loop state
|
|
||||||
missingPointsTotal = newMissingPointsTotal
|
missingPointsTotal = newMissingPointsTotal
|
||||||
|
|
||||||
// Completed
|
|
||||||
if (missingPointsTotal === 0) {
|
if (missingPointsTotal === 0) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -120,7 +111,6 @@ export class Search extends Workers {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stuck
|
|
||||||
if (stagnantLoop > stagnantLoopMax) {
|
if (stagnantLoop > stagnantLoopMax) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -130,48 +120,72 @@ export class Search extends Workers {
|
|||||||
stagnantLoop = 0
|
stagnantLoop = 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remainingQueries = queries.length - (i + 1)
|
||||||
|
const minBuffer = 20
|
||||||
|
if (missingPointsTotal > 0 && remainingQueries < minBuffer) {
|
||||||
|
this.bot.logger.warn(
|
||||||
|
isMobile,
|
||||||
|
'SEARCH-BING',
|
||||||
|
`Low query buffer while still missing points, regenerating | remainingQueries=${remainingQueries} | missing=${missingPointsTotal}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const extra = await queryCore.queryManager({
|
||||||
|
shuffle: true,
|
||||||
|
related: true,
|
||||||
|
langCode,
|
||||||
|
geoLocale: locale,
|
||||||
|
sourceOrder: this.bot.config.searchSettings.queryEngines
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
|
||||||
|
queries = [...new Set(merged)]
|
||||||
|
queries = this.bot.utils.shuffleArray(queries)
|
||||||
|
|
||||||
|
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool regenerated | count=${queries.length}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingPointsTotal > 0) {
|
if (missingPointsTotal > 0) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING',
|
'SEARCH-BING',
|
||||||
`Search completed but still missing points, generating extra searches | remaining=${missingPointsTotal}`
|
`Search completed but still missing points, continuing with regenerated queries | remaining=${missingPointsTotal}`
|
||||||
)
|
)
|
||||||
|
|
||||||
let i = 0
|
|
||||||
let stagnantLoop = 0
|
let stagnantLoop = 0
|
||||||
const stagnantLoopMax = 5
|
const stagnantLoopMax = 5
|
||||||
|
|
||||||
while (missingPointsTotal > 0) {
|
while (missingPointsTotal > 0) {
|
||||||
const query = queries[i++] as string
|
const extra = await queryCore.queryManager({
|
||||||
|
shuffle: true,
|
||||||
|
related: true,
|
||||||
|
langCode,
|
||||||
|
geoLocale: locale,
|
||||||
|
sourceOrder: this.bot.config.searchSettings.queryEngines
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
|
||||||
|
const newPool = [...new Set(merged)]
|
||||||
|
queries = this.bot.utils.shuffleArray(newPool)
|
||||||
|
|
||||||
this.bot.logger.debug(
|
this.bot.logger.debug(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING-EXTRA',
|
'SEARCH-BING-EXTRA',
|
||||||
`Fetching related terms for extra searches | baseQuery="${query}"`
|
`New query pool generated | count=${queries.length}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const relatedTerms = await queryCore.getBingRelatedTerms(query)
|
for (const query of queries) {
|
||||||
this.bot.logger.debug(
|
|
||||||
isMobile,
|
|
||||||
'SEARCH-BING-EXTRA',
|
|
||||||
`Related terms resolved | baseQuery="${query}" | count=${relatedTerms.length}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (relatedTerms.length > 3) {
|
|
||||||
for (const term of relatedTerms.slice(1, 3)) {
|
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING-EXTRA',
|
'SEARCH-BING-EXTRA',
|
||||||
`Extra search | remaining=${missingPointsTotal} | query="${term}"`
|
`Extra search | remaining=${missingPointsTotal} | query="${query}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
searchCounters = await this.bingSearch(page, term, isMobile)
|
searchCounters = await this.bingSearch(page, query, isMobile)
|
||||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||||
|
|
||||||
// Points gained for THIS extra query only
|
|
||||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||||
const gainedPoints = Math.max(0, rawGained)
|
const gainedPoints = Math.max(0, rawGained)
|
||||||
|
|
||||||
@@ -180,31 +194,27 @@ export class Search extends Workers {
|
|||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING-EXTRA',
|
'SEARCH-BING-EXTRA',
|
||||||
`No points gained for extra query ${stagnantLoop}/${stagnantLoopMax} | query="${term}" | remaining=${newMissingPointsTotal}`
|
`No points gained ${stagnantLoop}/${stagnantLoopMax} | query="${query}" | remaining=${newMissingPointsTotal}`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stagnantLoop = 0
|
stagnantLoop = 0
|
||||||
|
|
||||||
// Update global user data
|
|
||||||
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
||||||
this.bot.userData.currentPoints = newBalance
|
this.bot.userData.currentPoints = newBalance
|
||||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||||
|
|
||||||
// Track for return value
|
|
||||||
totalGainedPoints += gainedPoints
|
totalGainedPoints += gainedPoints
|
||||||
|
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING-EXTRA',
|
'SEARCH-BING-EXTRA',
|
||||||
`gainedPoints=${gainedPoints} points | query="${term}" | remaining=${newMissingPointsTotal}`,
|
`gainedPoints=${gainedPoints} points | query="${query}" | remaining=${newMissingPointsTotal}`,
|
||||||
'green'
|
'green'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update loop state
|
|
||||||
missingPointsTotal = newMissingPointsTotal
|
missingPointsTotal = newMissingPointsTotal
|
||||||
|
|
||||||
// Completed
|
|
||||||
if (missingPointsTotal === 0) {
|
if (missingPointsTotal === 0) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -214,12 +224,11 @@ export class Search extends Workers {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stuck again
|
|
||||||
if (stagnantLoop > stagnantLoopMax) {
|
if (stagnantLoop > stagnantLoopMax) {
|
||||||
this.bot.logger.warn(
|
this.bot.logger.warn(
|
||||||
isMobile,
|
isMobile,
|
||||||
'SEARCH-BING-EXTRA',
|
'SEARCH-BING-EXTRA',
|
||||||
`Search did not gain points for ${stagnantLoopMax} extra iterations, aborting extra searches`
|
`Search did not gain points for ${stagnantLoopMax} iterations, aborting extra searches`
|
||||||
)
|
)
|
||||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
@@ -232,7 +241,6 @@ export class Search extends Workers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||||
|
|
||||||
@@ -259,7 +267,6 @@ export class Search extends Workers {
|
|||||||
|
|
||||||
this.searchCount++
|
this.searchCount++
|
||||||
|
|
||||||
// Page fill seems to get more sluggish over time
|
|
||||||
if (this.searchCount % refreshThreshold === 0) {
|
if (this.searchCount % refreshThreshold === 0) {
|
||||||
this.bot.logger.info(
|
this.bot.logger.info(
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -271,7 +278,7 @@ export class Search extends Workers {
|
|||||||
|
|
||||||
await searchPage.goto(this.bingHome)
|
await searchPage.goto(this.bingHome)
|
||||||
await searchPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
await searchPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||||
await this.bot.browser.utils.tryDismissAllMessages(searchPage) // Not always the case but possible for new cookie headers
|
await this.bot.browser.utils.tryDismissAllMessages(searchPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bot.logger.debug(
|
this.bot.logger.debug(
|
||||||
@@ -402,11 +409,9 @@ export class Search extends Workers {
|
|||||||
await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime)
|
await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime)
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// Mobile
|
|
||||||
await page.goto(searchPageUrl)
|
await page.goto(searchPageUrl)
|
||||||
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page')
|
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page')
|
||||||
} else {
|
} else {
|
||||||
// Desktop
|
|
||||||
const newTab = await this.bot.browser.utils.getLatestTab(page)
|
const newTab = await this.bot.browser.utils.getLatestTab(page)
|
||||||
const newTabUrl = newTab.url()
|
const newTabUrl = newTab.url()
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export class SearchOnBing extends Workers {
|
|||||||
if (this.bot.config.searchOnBingLocalQueries) {
|
if (this.bot.config.searchOnBingLocalQueries) {
|
||||||
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'Using local queries config file')
|
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'Using local queries config file')
|
||||||
|
|
||||||
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8')
|
const data = fs.readFileSync(path.join(__dirname, '../bing-search-activity-queries.json'), 'utf8')
|
||||||
queries = JSON.parse(data)
|
queries = JSON.parse(data)
|
||||||
|
|
||||||
this.bot.logger.debug(
|
this.bot.logger.debug(
|
||||||
@@ -250,7 +250,7 @@ export class SearchOnBing extends Workers {
|
|||||||
// Fetch from the repo directly so the user doesn't need to redownload the script for the new activities
|
// 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({
|
const response = await this.bot.axios.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/queries.json'
|
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/bing-search-activity-queries.json'
|
||||||
})
|
})
|
||||||
queries = response.data
|
queries = response.data
|
||||||
|
|
||||||
|
|||||||
582
src/functions/bing-search-activity-queries.json
Normal file
582
src/functions/bing-search-activity-queries.json
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Houses near you",
|
||||||
|
"queries": [
|
||||||
|
"Houses near me",
|
||||||
|
"Homes for sale near me",
|
||||||
|
"Apartments near me",
|
||||||
|
"Real estate listings near me",
|
||||||
|
"Zillow homes near me",
|
||||||
|
"houses for rent near me"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Feeling symptoms?",
|
||||||
|
"queries": [
|
||||||
|
"Rash on forearm",
|
||||||
|
"Stuffy nose",
|
||||||
|
"Tickling cough",
|
||||||
|
"sore throat remedies",
|
||||||
|
"headache and nausea causes",
|
||||||
|
"fever symptoms adults"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Get your shopping done faster",
|
||||||
|
"queries": [
|
||||||
|
"Buy PS5",
|
||||||
|
"Buy Xbox",
|
||||||
|
"Chair deals",
|
||||||
|
"wireless mouse deals",
|
||||||
|
"best gaming headset price",
|
||||||
|
"laptop deals",
|
||||||
|
"buy office chair",
|
||||||
|
"SSD deals"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Translate anything",
|
||||||
|
"queries": [
|
||||||
|
"Translate welcome home to Korean",
|
||||||
|
"Translate welcome home to Japanese",
|
||||||
|
"Translate goodbye to Japanese",
|
||||||
|
"Translate good morning to Spanish",
|
||||||
|
"Translate thank you to French",
|
||||||
|
"Translate see you later to Italian"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Search the lyrics of a song",
|
||||||
|
"queries": [
|
||||||
|
"Debarge rhythm of the night lyrics",
|
||||||
|
"bohemian rhapsody lyrics",
|
||||||
|
"hotel california lyrics",
|
||||||
|
"blinding lights lyrics",
|
||||||
|
"lose yourself lyrics",
|
||||||
|
"smells like teen spirit lyrics"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Let's watch that movie again!",
|
||||||
|
"queries": [
|
||||||
|
"Alien movie",
|
||||||
|
"Aliens movie",
|
||||||
|
"Alien 3 movie",
|
||||||
|
"Predator movie",
|
||||||
|
"Terminator movie",
|
||||||
|
"John Wick movie",
|
||||||
|
"Interstellar movie",
|
||||||
|
"The Matrix movie"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Plan a quick getaway",
|
||||||
|
"queries": [
|
||||||
|
"Flights Amsterdam to Tokyo",
|
||||||
|
"Flights New York to Tokyo",
|
||||||
|
"cheap flights to paris",
|
||||||
|
"flights amsterdam to rome",
|
||||||
|
"last minute flight deals",
|
||||||
|
"direct flights from amsterdam",
|
||||||
|
"weekend getaway europe",
|
||||||
|
"best time to visit tokyo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Discover open job roles",
|
||||||
|
"queries": [
|
||||||
|
"jobs at Microsoft",
|
||||||
|
"Microsoft Job Openings",
|
||||||
|
"Jobs near me",
|
||||||
|
"jobs at Boeing worked",
|
||||||
|
"software engineer jobs near me",
|
||||||
|
"remote developer jobs",
|
||||||
|
"IT jobs netherlands",
|
||||||
|
"customer support jobs near me"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "You can track your package",
|
||||||
|
"queries": [
|
||||||
|
"USPS tracking",
|
||||||
|
"UPS tracking",
|
||||||
|
"DHL tracking",
|
||||||
|
"FedEx tracking",
|
||||||
|
"track my package",
|
||||||
|
"international package tracking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Find somewhere new to explore",
|
||||||
|
"queries": [
|
||||||
|
"Directions to Berlin",
|
||||||
|
"Directions to Tokyo",
|
||||||
|
"Directions to New York",
|
||||||
|
"things to do in berlin",
|
||||||
|
"tourist attractions tokyo",
|
||||||
|
"best places to visit in new york",
|
||||||
|
"hidden gems near me",
|
||||||
|
"day trips near me"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Too tired to cook tonight?",
|
||||||
|
"queries": [
|
||||||
|
"KFC near me",
|
||||||
|
"Burger King near me",
|
||||||
|
"McDonalds near me",
|
||||||
|
"pizza delivery near me",
|
||||||
|
"restaurants open now",
|
||||||
|
"best takeout near me",
|
||||||
|
"quick dinner ideas",
|
||||||
|
"easy dinner recipes"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Quickly convert your money",
|
||||||
|
"queries": [
|
||||||
|
"convert 250 USD to yen",
|
||||||
|
"convert 500 USD to yen",
|
||||||
|
"usd to eur",
|
||||||
|
"gbp to eur",
|
||||||
|
"eur to jpy",
|
||||||
|
"currency converter",
|
||||||
|
"exchange rate today",
|
||||||
|
"1000 yen to euro"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Learn to cook a new recipe",
|
||||||
|
"queries": [
|
||||||
|
"How to cook ratatouille",
|
||||||
|
"How to cook lasagna",
|
||||||
|
"easy pasta recipe",
|
||||||
|
"how to make pancakes",
|
||||||
|
"how to make fried rice",
|
||||||
|
"simple chicken recipe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Find places to stay!",
|
||||||
|
"queries": [
|
||||||
|
"Hotels Berlin Germany",
|
||||||
|
"Hotels Amsterdam Netherlands",
|
||||||
|
"hotels in paris",
|
||||||
|
"best hotels in tokyo",
|
||||||
|
"cheap hotels london",
|
||||||
|
"places to stay in barcelona",
|
||||||
|
"hotel deals",
|
||||||
|
"booking hotels near me"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "How's the economy?",
|
||||||
|
"queries": [
|
||||||
|
"sp 500",
|
||||||
|
"nasdaq",
|
||||||
|
"dow jones today",
|
||||||
|
"inflation rate europe",
|
||||||
|
"interest rates today",
|
||||||
|
"stock market today",
|
||||||
|
"economic news",
|
||||||
|
"recession forecast"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Who won?",
|
||||||
|
"queries": [
|
||||||
|
"braves score",
|
||||||
|
"champions league results",
|
||||||
|
"premier league results",
|
||||||
|
"nba score",
|
||||||
|
"formula 1 winner",
|
||||||
|
"latest football scores",
|
||||||
|
"ucl final winner",
|
||||||
|
"world cup final result"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Gaming time",
|
||||||
|
"queries": [
|
||||||
|
"Overwatch video game",
|
||||||
|
"Call of duty video game",
|
||||||
|
"best games 2025",
|
||||||
|
"top xbox games",
|
||||||
|
"popular steam games",
|
||||||
|
"new pc games",
|
||||||
|
"game reviews",
|
||||||
|
"best co-op games"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Expand your vocabulary",
|
||||||
|
"queries": [
|
||||||
|
"definition definition",
|
||||||
|
"meaning of serendipity",
|
||||||
|
"define nostalgia",
|
||||||
|
"synonym for happy",
|
||||||
|
"define eloquent",
|
||||||
|
"what does epiphany mean",
|
||||||
|
"word of the day",
|
||||||
|
"define immaculate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "What time is it?",
|
||||||
|
"queries": [
|
||||||
|
"Japan time",
|
||||||
|
"New York time",
|
||||||
|
"time in london",
|
||||||
|
"time in tokyo",
|
||||||
|
"current time in amsterdam",
|
||||||
|
"time in los angeles"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Find deals on Bing",
|
||||||
|
"queries": [
|
||||||
|
"best laptop deals",
|
||||||
|
"tech deals today",
|
||||||
|
"wireless earbuds deals",
|
||||||
|
"gaming chair deals",
|
||||||
|
"discount codes electronics",
|
||||||
|
"best amazon deals today",
|
||||||
|
"smartphone deals",
|
||||||
|
"ssd deals"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Prepare for the weather",
|
||||||
|
"queries": [
|
||||||
|
"weather tomorrow",
|
||||||
|
"weekly weather forecast",
|
||||||
|
"rain forecast today",
|
||||||
|
"weather in amsterdam",
|
||||||
|
"storm forecast europe",
|
||||||
|
"uv index today",
|
||||||
|
"temperature this weekend",
|
||||||
|
"snow forecast"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Track your delivery",
|
||||||
|
"queries": [
|
||||||
|
"track my package",
|
||||||
|
"postnl track and trace",
|
||||||
|
"dhl parcel tracking",
|
||||||
|
"ups tracking",
|
||||||
|
"fedex tracking",
|
||||||
|
"usps tracking",
|
||||||
|
"parcel tracking",
|
||||||
|
"international package tracking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Explore a new spot today",
|
||||||
|
"queries": [
|
||||||
|
"places to visit near me",
|
||||||
|
"things to do near me",
|
||||||
|
"hidden gems netherlands",
|
||||||
|
"best museums near me",
|
||||||
|
"parks near me",
|
||||||
|
"tourist attractions nearby",
|
||||||
|
"best cafes near me",
|
||||||
|
"day trip ideas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Maisons près de chez vous",
|
||||||
|
"queries": [
|
||||||
|
"Maisons près de chez moi",
|
||||||
|
"Maisons à vendre près de chez moi",
|
||||||
|
"Appartements près de chez moi",
|
||||||
|
"Annonces immobilières près de chez moi",
|
||||||
|
"Maisons à louer près de chez moi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Vous ressentez des symptômes ?",
|
||||||
|
"queries": [
|
||||||
|
"Éruption cutanée sur l'avant-bras",
|
||||||
|
"Nez bouché",
|
||||||
|
"Toux chatouilleuse",
|
||||||
|
"mal de gorge remèdes",
|
||||||
|
"maux de tête causes",
|
||||||
|
"symptômes de la grippe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Faites vos achats plus vite",
|
||||||
|
"queries": [
|
||||||
|
"Acheter une PS5",
|
||||||
|
"Acheter une Xbox",
|
||||||
|
"Offres sur les chaises",
|
||||||
|
"offres ordinateur portable",
|
||||||
|
"meilleures offres casque",
|
||||||
|
"acheter souris sans fil",
|
||||||
|
"promotions ssd",
|
||||||
|
"bons plans tech"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Traduisez tout !",
|
||||||
|
"queries": [
|
||||||
|
"Traduction bienvenue à la maison en coréen",
|
||||||
|
"Traduction bienvenue à la maison en japonais",
|
||||||
|
"Traduction au revoir en japonais",
|
||||||
|
"Traduire bonjour en espagnol",
|
||||||
|
"Traduire merci en anglais",
|
||||||
|
"Traduire à plus tard en italien"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rechercher paroles de chanson",
|
||||||
|
"queries": [
|
||||||
|
"Paroles de Debarge rhythm of the night",
|
||||||
|
"paroles bohemian rhapsody",
|
||||||
|
"paroles hotel california",
|
||||||
|
"paroles blinding lights",
|
||||||
|
"paroles lose yourself",
|
||||||
|
"paroles smells like teen spirit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Et si nous regardions ce film une nouvelle fois?",
|
||||||
|
"queries": [
|
||||||
|
"Alien film",
|
||||||
|
"Film Aliens",
|
||||||
|
"Film Alien 3",
|
||||||
|
"Film Predator",
|
||||||
|
"Film Terminator",
|
||||||
|
"Film John Wick",
|
||||||
|
"Film Interstellar",
|
||||||
|
"Film Matrix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Planifiez une petite escapade",
|
||||||
|
"queries": [
|
||||||
|
"Vols Amsterdam-Tokyo",
|
||||||
|
"Vols New York-Tokyo",
|
||||||
|
"vols pas chers paris",
|
||||||
|
"vols amsterdam rome",
|
||||||
|
"offres vols dernière minute",
|
||||||
|
"week-end en europe",
|
||||||
|
"vols directs depuis amsterdam"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Consulter postes à pourvoir",
|
||||||
|
"queries": [
|
||||||
|
"emplois chez Microsoft",
|
||||||
|
"Offres d'emploi Microsoft",
|
||||||
|
"Emplois près de chez moi",
|
||||||
|
"emplois chez Boeing",
|
||||||
|
"emplois développeur à distance",
|
||||||
|
"emplois informatique pays-bas",
|
||||||
|
"offres d'emploi près de chez moi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Vous pouvez suivre votre colis",
|
||||||
|
"queries": [
|
||||||
|
"Suivi Chronopost",
|
||||||
|
"suivi colis",
|
||||||
|
"suivi DHL",
|
||||||
|
"suivi UPS",
|
||||||
|
"suivi FedEx",
|
||||||
|
"suivi international colis"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Trouver un endroit à découvrir",
|
||||||
|
"queries": [
|
||||||
|
"Itinéraire vers Berlin",
|
||||||
|
"Itinéraire vers Tokyo",
|
||||||
|
"Itinéraire vers New York",
|
||||||
|
"que faire à berlin",
|
||||||
|
"attractions tokyo",
|
||||||
|
"meilleurs endroits à visiter à new york",
|
||||||
|
"endroits à visiter près de chez moi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"livraison pizza près de chez moi",
|
||||||
|
"restaurants ouverts maintenant",
|
||||||
|
"idées dîner rapide",
|
||||||
|
"quoi manger ce soir"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Convertissez rapidement votre argent",
|
||||||
|
"queries": [
|
||||||
|
"convertir 250 EUR en yen",
|
||||||
|
"convertir 500 EUR en yen",
|
||||||
|
"usd en eur",
|
||||||
|
"gbp en eur",
|
||||||
|
"eur en jpy",
|
||||||
|
"convertisseur de devises",
|
||||||
|
"taux de change aujourd'hui",
|
||||||
|
"1000 yen en euro"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Apprenez à cuisiner une nouvelle recette",
|
||||||
|
"queries": [
|
||||||
|
"Comment faire cuire la ratatouille",
|
||||||
|
"Comment faire cuire les lasagnes",
|
||||||
|
"recette pâtes facile",
|
||||||
|
"comment faire des crêpes",
|
||||||
|
"recette riz sauté",
|
||||||
|
"recette poulet simple"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Trouvez des emplacements pour rester!",
|
||||||
|
"queries": [
|
||||||
|
"Hôtels Berlin Allemagne",
|
||||||
|
"Hôtels Amsterdam Pays-Bas",
|
||||||
|
"hôtels paris",
|
||||||
|
"meilleurs hôtels tokyo",
|
||||||
|
"hôtels pas chers londres",
|
||||||
|
"hébergement barcelone",
|
||||||
|
"offres hôtels",
|
||||||
|
"hôtels près de chez moi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Comment se porte l'économie ?",
|
||||||
|
"queries": [
|
||||||
|
"CAC 40",
|
||||||
|
"indice dax",
|
||||||
|
"dow jones aujourd'hui",
|
||||||
|
"inflation europe",
|
||||||
|
"taux d'intérêt aujourd'hui",
|
||||||
|
"marché boursier aujourd'hui",
|
||||||
|
"actualités économie",
|
||||||
|
"prévisions récession"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Qui a gagné ?",
|
||||||
|
"queries": [
|
||||||
|
"score du Paris Saint-Germain",
|
||||||
|
"résultats ligue des champions",
|
||||||
|
"résultats premier league",
|
||||||
|
"score nba",
|
||||||
|
"vainqueur formule 1",
|
||||||
|
"derniers scores football",
|
||||||
|
"vainqueur finale ldc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Temps de jeu",
|
||||||
|
"queries": [
|
||||||
|
"Jeu vidéo Overwatch",
|
||||||
|
"Jeu vidéo Call of Duty",
|
||||||
|
"meilleurs jeux 2025",
|
||||||
|
"top jeux xbox",
|
||||||
|
"jeux steam populaires",
|
||||||
|
"nouveaux jeux pc",
|
||||||
|
"avis jeux vidéo",
|
||||||
|
"meilleurs jeux coop"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Enrichissez votre vocabulaire",
|
||||||
|
"queries": [
|
||||||
|
"definition definition",
|
||||||
|
"signification sérendipité",
|
||||||
|
"définir nostalgie",
|
||||||
|
"synonyme heureux",
|
||||||
|
"définir éloquent",
|
||||||
|
"mot du jour",
|
||||||
|
"que veut dire épiphanie"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Quelle heure est-il ?",
|
||||||
|
"queries": [
|
||||||
|
"Heure du Japon",
|
||||||
|
"Heure de New York",
|
||||||
|
"heure de londres",
|
||||||
|
"heure de tokyo",
|
||||||
|
"heure actuelle amsterdam",
|
||||||
|
"heure de los angeles"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Vérifier la météo",
|
||||||
|
"queries": [
|
||||||
|
"Météo de Paris",
|
||||||
|
"Météo de la France",
|
||||||
|
"météo demain",
|
||||||
|
"prévisions météo semaine",
|
||||||
|
"météo amsterdam",
|
||||||
|
"risque de pluie aujourd'hui"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tenez-vous informé des sujets d'actualité",
|
||||||
|
"queries": [
|
||||||
|
"Augmentation Impots",
|
||||||
|
"Mort célébrité",
|
||||||
|
"actualités france",
|
||||||
|
"actualité internationale",
|
||||||
|
"dernières nouvelles économie",
|
||||||
|
"news technologie"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Préparez-vous pour la météo",
|
||||||
|
"queries": [
|
||||||
|
"météo demain",
|
||||||
|
"prévisions météo semaine",
|
||||||
|
"météo amsterdam",
|
||||||
|
"risque de pluie aujourd'hui",
|
||||||
|
"indice uv aujourd'hui",
|
||||||
|
"température ce week-end",
|
||||||
|
"alerte tempête"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Suivez votre livraison",
|
||||||
|
"queries": [
|
||||||
|
"suivi colis",
|
||||||
|
"postnl suivi colis",
|
||||||
|
"suivi DHL colis",
|
||||||
|
"suivi UPS",
|
||||||
|
"suivi FedEx",
|
||||||
|
"suivi international colis",
|
||||||
|
"suivre ma livraison"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Trouvez des offres sur Bing",
|
||||||
|
"queries": [
|
||||||
|
"meilleures offres ordinateur portable",
|
||||||
|
"bons plans tech",
|
||||||
|
"promotions écouteurs",
|
||||||
|
"offres chaise gamer",
|
||||||
|
"codes promo électronique",
|
||||||
|
"meilleures offres amazon aujourd'hui"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Explorez un nouvel endroit aujourd'hui",
|
||||||
|
"queries": [
|
||||||
|
"endroits à visiter près de chez moi",
|
||||||
|
"que faire près de chez moi",
|
||||||
|
"endroits insolites pays-bas",
|
||||||
|
"meilleurs musées près de chez moi",
|
||||||
|
"parcs près de chez moi",
|
||||||
|
"attractions touristiques à proximité",
|
||||||
|
"meilleurs cafés près de chez moi"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
116
src/functions/search-queries.json
Normal file
116
src/functions/search-queries.json
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
[
|
||||||
|
"weather tomorrow",
|
||||||
|
"how to cook pasta",
|
||||||
|
"best movies 2024",
|
||||||
|
"latest tech news",
|
||||||
|
"how tall is the eiffel tower",
|
||||||
|
"easy dinner recipes",
|
||||||
|
"what time is it in japan",
|
||||||
|
"how does photosynthesis work",
|
||||||
|
"best budget smartphones",
|
||||||
|
"coffee vs espresso difference",
|
||||||
|
"how to improve wifi signal",
|
||||||
|
"popular netflix series",
|
||||||
|
"how many calories in an apple",
|
||||||
|
"world population today",
|
||||||
|
"best free pc games",
|
||||||
|
"how to clean a keyboard",
|
||||||
|
"what is artificial intelligence",
|
||||||
|
"simple home workouts",
|
||||||
|
"how long do cats live",
|
||||||
|
"famous paintings in museums",
|
||||||
|
"how to boil eggs",
|
||||||
|
"latest windows updates",
|
||||||
|
"how to screenshot on windows",
|
||||||
|
"best travel destinations europe",
|
||||||
|
"what is cloud computing",
|
||||||
|
"how to save money monthly",
|
||||||
|
"best youtube channels",
|
||||||
|
"how fast is light",
|
||||||
|
"how to learn programming",
|
||||||
|
"popular board games",
|
||||||
|
"how to make pancakes",
|
||||||
|
"capital cities of europe",
|
||||||
|
"how does a vpn work",
|
||||||
|
"best productivity apps",
|
||||||
|
"how to grow plants indoors",
|
||||||
|
"difference between hdd and ssd",
|
||||||
|
"how to fix slow computer",
|
||||||
|
"most streamed songs",
|
||||||
|
"how to tie a tie",
|
||||||
|
"what causes rain",
|
||||||
|
"best laptops for students",
|
||||||
|
"how to reset router",
|
||||||
|
"healthy breakfast ideas",
|
||||||
|
"how many continents are there",
|
||||||
|
"latest smartphone features",
|
||||||
|
"how to meditate beginners",
|
||||||
|
"what is renewable energy",
|
||||||
|
"best pc accessories",
|
||||||
|
"how to clean glasses",
|
||||||
|
"famous landmarks worldwide",
|
||||||
|
"how to make coffee at home",
|
||||||
|
"what is machine learning",
|
||||||
|
"best programming languages",
|
||||||
|
"how to backup files",
|
||||||
|
"how does bluetooth work",
|
||||||
|
"top video games right now",
|
||||||
|
"how to improve sleep quality",
|
||||||
|
"what is cryptocurrency",
|
||||||
|
"easy lunch ideas",
|
||||||
|
"how to check internet speed",
|
||||||
|
"best noise cancelling headphones",
|
||||||
|
"how to take screenshots on mac",
|
||||||
|
"what is the milky way",
|
||||||
|
"how to organize files",
|
||||||
|
"popular mobile apps",
|
||||||
|
"how to learn faster",
|
||||||
|
"how does gps work",
|
||||||
|
"best free antivirus",
|
||||||
|
"how to clean a monitor",
|
||||||
|
"what is an electric car",
|
||||||
|
"simple math tricks",
|
||||||
|
"how to update drivers",
|
||||||
|
"famous scientists",
|
||||||
|
"how to cook rice",
|
||||||
|
"what is the tallest mountain",
|
||||||
|
"best tv shows all time",
|
||||||
|
"how to improve typing speed",
|
||||||
|
"how does solar power work",
|
||||||
|
"easy dessert recipes",
|
||||||
|
"how to fix bluetooth issues",
|
||||||
|
"what is the internet",
|
||||||
|
"best pc keyboards",
|
||||||
|
"how to stay focused",
|
||||||
|
"popular science facts",
|
||||||
|
"how to convert files to pdf",
|
||||||
|
"how long does it take to sleep",
|
||||||
|
"best travel tips",
|
||||||
|
"how to clean headphones",
|
||||||
|
"what is open source software",
|
||||||
|
"how to manage time better",
|
||||||
|
"latest gaming news",
|
||||||
|
"how to check laptop temperature",
|
||||||
|
"what is a firewall",
|
||||||
|
"easy meal prep ideas",
|
||||||
|
"how to reduce eye strain",
|
||||||
|
"best budget headphones",
|
||||||
|
"how does email work",
|
||||||
|
"what is virtual reality",
|
||||||
|
"how to compress files",
|
||||||
|
"popular programming tools",
|
||||||
|
"how to improve concentration",
|
||||||
|
"how to make smoothies",
|
||||||
|
"best desk setup ideas",
|
||||||
|
"how to block ads",
|
||||||
|
"what is 5g technology",
|
||||||
|
"how to clean a mouse",
|
||||||
|
"famous world wonders",
|
||||||
|
"how to improve battery life",
|
||||||
|
"best cloud storage services",
|
||||||
|
"how to learn a new language",
|
||||||
|
"what is dark mode",
|
||||||
|
"how to clear browser cache",
|
||||||
|
"popular tech podcasts",
|
||||||
|
"how to stay motivated"
|
||||||
|
]
|
||||||
34
src/index.ts
34
src/index.ts
@@ -12,6 +12,7 @@ import BrowserUtils from './browser/BrowserUtils'
|
|||||||
import { IpcLog, Logger } from './logging/Logger'
|
import { IpcLog, Logger } from './logging/Logger'
|
||||||
import Utils from './util/Utils'
|
import Utils from './util/Utils'
|
||||||
import { loadAccounts, loadConfig } from './util/Load'
|
import { loadAccounts, loadConfig } from './util/Load'
|
||||||
|
import { checkNodeVersion } from './util/Validator'
|
||||||
|
|
||||||
import { Login } from './browser/auth/Login'
|
import { Login } from './browser/auth/Login'
|
||||||
import { Workers } from './functions/Workers'
|
import { Workers } from './functions/Workers'
|
||||||
@@ -27,7 +28,7 @@ import type { AppDashboardData } from './interface/AppDashBoardData'
|
|||||||
|
|
||||||
interface ExecutionContext {
|
interface ExecutionContext {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
accountEmail: string
|
account: Account
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BrowserSession {
|
interface BrowserSession {
|
||||||
@@ -50,7 +51,7 @@ const executionContext = new AsyncLocalStorage<ExecutionContext>()
|
|||||||
export function getCurrentContext(): ExecutionContext {
|
export function getCurrentContext(): ExecutionContext {
|
||||||
const context = executionContext.getStore()
|
const context = executionContext.getStore()
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return { isMobile: false, accountEmail: 'unknown' }
|
return { isMobile: false, account: {} as any }
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,7 @@ async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
|
|||||||
interface UserData {
|
interface UserData {
|
||||||
userName: string
|
userName: string
|
||||||
geoLocale: string
|
geoLocale: string
|
||||||
|
langCode: string
|
||||||
initialPoints: number
|
initialPoints: number
|
||||||
currentPoints: number
|
currentPoints: number
|
||||||
gainedPoints: number
|
gainedPoints: number
|
||||||
@@ -99,7 +101,8 @@ export class MicrosoftRewardsBot {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.userData = {
|
this.userData = {
|
||||||
userName: '',
|
userName: '',
|
||||||
geoLocale: '',
|
geoLocale: 'US',
|
||||||
|
langCode: 'en',
|
||||||
initialPoints: 0,
|
initialPoints: 0,
|
||||||
currentPoints: 0,
|
currentPoints: 0,
|
||||||
gainedPoints: 0
|
gainedPoints: 0
|
||||||
@@ -134,7 +137,7 @@ export class MicrosoftRewardsBot {
|
|||||||
this.logger.info(
|
this.logger.info(
|
||||||
'main',
|
'main',
|
||||||
'RUN-START',
|
'RUN-START',
|
||||||
`Starting Microsoft Rewards bot| v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}`
|
`Starting Microsoft Rewards Script | v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.config.clusters > 1) {
|
if (this.config.clusters > 1) {
|
||||||
@@ -185,11 +188,14 @@ export class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => {
|
const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => {
|
||||||
const { pid } = worker.process
|
const { pid } = worker.process
|
||||||
|
|
||||||
if (!pid || this.exitedWorkers.includes(pid)) return
|
|
||||||
else this.exitedWorkers.push(pid)
|
|
||||||
|
|
||||||
this.activeWorkers -= 1
|
this.activeWorkers -= 1
|
||||||
|
|
||||||
|
if (!pid || this.exitedWorkers.includes(pid)) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
this.exitedWorkers.push(pid)
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'main',
|
'main',
|
||||||
`CLUSTER-WORKER-${label.toUpperCase()}`,
|
`CLUSTER-WORKER-${label.toUpperCase()}`,
|
||||||
@@ -233,6 +239,7 @@ export class MicrosoftRewardsBot {
|
|||||||
if (process.send) {
|
if (process.send) {
|
||||||
process.send({ __stats: stats })
|
process.send({ __stats: stats })
|
||||||
}
|
}
|
||||||
|
|
||||||
process.disconnect()
|
process.disconnect()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -355,14 +362,14 @@ export class MicrosoftRewardsBot {
|
|||||||
let mobileContextClosed = false
|
let mobileContextClosed = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
return await executionContext.run({ isMobile: true, account }, async () => {
|
||||||
mobileSession = await this.browserFactory.createBrowser(account.proxy, accountEmail)
|
mobileSession = await this.browserFactory.createBrowser(account)
|
||||||
const initialContext: BrowserContext = mobileSession.context
|
const initialContext: BrowserContext = mobileSession.context
|
||||||
this.mainMobilePage = await initialContext.newPage()
|
this.mainMobilePage = await initialContext.newPage()
|
||||||
|
|
||||||
this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`)
|
this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`)
|
||||||
|
|
||||||
await this.login.login(this.mainMobilePage, accountEmail, account.password, account.totp)
|
await this.login.login(this.mainMobilePage, account)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail)
|
this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail)
|
||||||
@@ -410,6 +417,7 @@ export class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData)
|
if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData)
|
||||||
if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage)
|
if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage)
|
||||||
|
if (this.config.workers.doSpecialPromotions) await this.workers.doSpecialPromotions(data)
|
||||||
if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage)
|
if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage)
|
||||||
if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn()
|
if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn()
|
||||||
if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn()
|
if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn()
|
||||||
@@ -448,7 +456,7 @@ export class MicrosoftRewardsBot {
|
|||||||
} finally {
|
} finally {
|
||||||
if (mobileSession && !mobileContextClosed) {
|
if (mobileSession && !mobileContextClosed) {
|
||||||
try {
|
try {
|
||||||
await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
await executionContext.run({ isMobile: true, account }, async () => {
|
||||||
await this.browser.func.closeBrowser(mobileSession!.context, accountEmail)
|
await this.browser.func.closeBrowser(mobileSession!.context, accountEmail)
|
||||||
})
|
})
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -460,6 +468,8 @@ export class MicrosoftRewardsBot {
|
|||||||
export { executionContext }
|
export { executionContext }
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
// Check before doing anything
|
||||||
|
checkNodeVersion()
|
||||||
const rewardsBot = new MicrosoftRewardsBot()
|
const rewardsBot = new MicrosoftRewardsBot()
|
||||||
|
|
||||||
process.on('beforeExit', () => {
|
process.on('beforeExit', () => {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
export interface Account {
|
export interface Account {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
totp?: string
|
totpSecret?: string
|
||||||
|
recoveryEmail: string
|
||||||
geoLocale: 'auto' | string
|
geoLocale: 'auto' | string
|
||||||
|
langCode: 'en' | string
|
||||||
proxy: AccountProxy
|
proxy: AccountProxy
|
||||||
|
saveFingerprint: ConfigSaveFingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountProxy {
|
export interface AccountProxy {
|
||||||
@@ -13,3 +16,8 @@ export interface AccountProxy {
|
|||||||
password: string
|
password: string
|
||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigSaveFingerprint {
|
||||||
|
mobile: boolean
|
||||||
|
desktop: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export interface Config {
|
|||||||
runOnZeroPoints: boolean
|
runOnZeroPoints: boolean
|
||||||
clusters: number
|
clusters: number
|
||||||
errorDiagnostics: boolean
|
errorDiagnostics: boolean
|
||||||
saveFingerprint: ConfigSaveFingerprint
|
|
||||||
workers: ConfigWorkers
|
workers: ConfigWorkers
|
||||||
searchOnBingLocalQueries: boolean
|
searchOnBingLocalQueries: boolean
|
||||||
globalTimeout: number | string
|
globalTimeout: number | string
|
||||||
@@ -16,15 +15,13 @@ export interface Config {
|
|||||||
webhook: ConfigWebhook
|
webhook: ConfigWebhook
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigSaveFingerprint {
|
export type QueryEngine = 'google' | 'wikipedia' | 'reddit' | 'local'
|
||||||
mobile: boolean
|
|
||||||
desktop: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigSearchSettings {
|
export interface ConfigSearchSettings {
|
||||||
scrollRandomResults: boolean
|
scrollRandomResults: boolean
|
||||||
clickRandomResults: boolean
|
clickRandomResults: boolean
|
||||||
parallelSearching: boolean
|
parallelSearching: boolean
|
||||||
|
queryEngines: QueryEngine[]
|
||||||
searchResultVisitTime: number | string
|
searchResultVisitTime: number | string
|
||||||
searchDelay: ConfigDelay
|
searchDelay: ConfigDelay
|
||||||
readDelay: ConfigDelay
|
readDelay: ConfigDelay
|
||||||
@@ -41,6 +38,7 @@ export interface ConfigProxy {
|
|||||||
|
|
||||||
export interface ConfigWorkers {
|
export interface ConfigWorkers {
|
||||||
doDailySet: boolean
|
doDailySet: boolean
|
||||||
|
doSpecialPromotions: boolean
|
||||||
doMorePromotions: boolean
|
doMorePromotions: boolean
|
||||||
doPunchCards: boolean
|
doPunchCards: boolean
|
||||||
doAppPromotions: boolean
|
doAppPromotions: boolean
|
||||||
|
|||||||
@@ -94,3 +94,23 @@ export interface BingTrendingImage {
|
|||||||
export interface BingTrendingQuery {
|
export interface BingTrendingQuery {
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WikipediaTopResponse {
|
||||||
|
items: Array<{
|
||||||
|
articles: Array<{
|
||||||
|
article: string
|
||||||
|
views: number
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RedditListing {
|
||||||
|
data: {
|
||||||
|
children: Array<{
|
||||||
|
data: {
|
||||||
|
title: string
|
||||||
|
over_18: boolean
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ export class Logger {
|
|||||||
const now = new Date().toLocaleString()
|
const now = new Date().toLocaleString()
|
||||||
const formatted = formatMessage(message)
|
const formatted = formatMessage(message)
|
||||||
|
|
||||||
|
const userName = this.bot.userData.userName ? this.bot.userData.userName : 'MAIN'
|
||||||
|
|
||||||
const levelTag = level.toUpperCase()
|
const levelTag = level.toUpperCase()
|
||||||
const cleanMsg = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${platformText(
|
const cleanMsg = `[${now}] [${userName}] [${levelTag}] ${platformText(isMobile)} [${title}] ${formatted}`
|
||||||
isMobile
|
|
||||||
)} [${title}] ${formatted}`
|
|
||||||
|
|
||||||
const config = this.bot.config
|
const config = this.bot.config
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const badge = platformBadge(isMobile)
|
const badge = platformBadge(isMobile)
|
||||||
const consoleStr = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${badge} [${title}] ${formatted}`
|
const consoleStr = `[${now}] [${userName}] [${levelTag}] ${badge} [${title}] ${formatted}`
|
||||||
|
|
||||||
let logColor: ColorKey | undefined = color
|
let logColor: ColorKey | undefined = color
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
|||||||
import axiosRetry from 'axios-retry'
|
import axiosRetry from 'axios-retry'
|
||||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import type { AccountProxy } from '../interface/Account'
|
import type { AccountProxy } from '../interface/Account'
|
||||||
|
|
||||||
@@ -36,7 +37,9 @@ class AxiosClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> {
|
private getAgentForProxy(
|
||||||
|
proxyConfig: AccountProxy
|
||||||
|
): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
|
||||||
const { url: baseUrl, port, username, password } = proxyConfig
|
const { url: baseUrl, port, username, password } = proxyConfig
|
||||||
|
|
||||||
let urlObj: URL
|
let urlObj: URL
|
||||||
@@ -67,8 +70,11 @@ class AxiosClient {
|
|||||||
return new HttpProxyAgent(proxyUrl)
|
return new HttpProxyAgent(proxyUrl)
|
||||||
case 'https:':
|
case 'https:':
|
||||||
return new HttpsProxyAgent(proxyUrl)
|
return new HttpsProxyAgent(proxyUrl)
|
||||||
|
case 'socks4:':
|
||||||
|
case 'socks5:':
|
||||||
|
return new SocksProxyAgent(proxyUrl)
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) is supported!`)
|
throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) and SOCKS4/5 are supported!`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import type { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import type { Account } from '../interface/Account'
|
import type { Account, ConfigSaveFingerprint } from '../interface/Account'
|
||||||
import type { Config, ConfigSaveFingerprint } from '../interface/Config'
|
import type { Config } from '../interface/Config'
|
||||||
|
import { validateAccounts, validateConfig } from './Validator'
|
||||||
|
|
||||||
let configCache: Config
|
let configCache: Config
|
||||||
|
|
||||||
@@ -18,8 +19,11 @@ export function loadAccounts(): Account[] {
|
|||||||
|
|
||||||
const accountDir = path.join(__dirname, '../', file)
|
const accountDir = path.join(__dirname, '../', file)
|
||||||
const accounts = fs.readFileSync(accountDir, 'utf-8')
|
const accounts = fs.readFileSync(accountDir, 'utf-8')
|
||||||
|
const accountsData = JSON.parse(accounts)
|
||||||
|
|
||||||
return JSON.parse(accounts)
|
validateAccounts(accountsData)
|
||||||
|
|
||||||
|
return accountsData
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error as string)
|
throw new Error(error as string)
|
||||||
}
|
}
|
||||||
@@ -35,6 +39,8 @@ export function loadConfig(): Config {
|
|||||||
const config = fs.readFileSync(configDir, 'utf-8')
|
const config = fs.readFileSync(configDir, 'utf-8')
|
||||||
|
|
||||||
const configData = JSON.parse(config)
|
const configData = JSON.parse(config)
|
||||||
|
validateConfig(configData)
|
||||||
|
|
||||||
configCache = configData
|
configCache = configData
|
||||||
|
|
||||||
return configData
|
return configData
|
||||||
|
|||||||
@@ -21,10 +21,19 @@ export default class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shuffleArray<T>(array: T[]): T[] {
|
shuffleArray<T>(array: T[]): T[] {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1))
|
||||||
|
|
||||||
|
const a = array[i]
|
||||||
|
const b = array[j]
|
||||||
|
|
||||||
|
if (a === undefined || b === undefined) continue
|
||||||
|
|
||||||
|
array[i] = b
|
||||||
|
array[j] = a
|
||||||
|
}
|
||||||
|
|
||||||
return array
|
return array
|
||||||
.map(value => ({ value, sort: Math.random() }))
|
|
||||||
.sort((a, b) => a.sort - b.sort)
|
|
||||||
.map(({ value }) => value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
randomNumber(min: number, max: number): number {
|
randomNumber(min: number, max: number): number {
|
||||||
|
|||||||
131
src/util/Validator.ts
Normal file
131
src/util/Validator.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import semver from 'semver'
|
||||||
|
import pkg from '../../package.json'
|
||||||
|
|
||||||
|
import { Config } from '../interface/Config'
|
||||||
|
import { Account } from '../interface/Account'
|
||||||
|
|
||||||
|
const NumberOrString = z.union([z.number(), z.string()])
|
||||||
|
|
||||||
|
const LogFilterSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
mode: z.enum(['whitelist', 'blacklist']),
|
||||||
|
levels: z.array(z.enum(['debug', 'info', 'warn', 'error'])).optional(),
|
||||||
|
keywords: z.array(z.string()).optional(),
|
||||||
|
regexPatterns: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
const DelaySchema = z.object({
|
||||||
|
min: NumberOrString,
|
||||||
|
max: NumberOrString
|
||||||
|
})
|
||||||
|
|
||||||
|
const QueryEngineSchema = z.enum(['google', 'wikipedia', 'reddit', 'local'])
|
||||||
|
|
||||||
|
// Webhook
|
||||||
|
const WebhookSchema = z.object({
|
||||||
|
discord: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
url: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
ntfy: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
url: z.string(),
|
||||||
|
topic: z.string().optional(),
|
||||||
|
token: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
priority: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
webhookLogFilter: LogFilterSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
// Config
|
||||||
|
export const ConfigSchema = z.object({
|
||||||
|
baseURL: z.string(),
|
||||||
|
sessionPath: z.string(),
|
||||||
|
headless: z.boolean(),
|
||||||
|
runOnZeroPoints: z.boolean(),
|
||||||
|
clusters: z.number().int().nonnegative(),
|
||||||
|
errorDiagnostics: z.boolean(),
|
||||||
|
workers: z.object({
|
||||||
|
doDailySet: z.boolean(),
|
||||||
|
doSpecialPromotions: z.boolean(),
|
||||||
|
doMorePromotions: z.boolean(),
|
||||||
|
doPunchCards: z.boolean(),
|
||||||
|
doAppPromotions: z.boolean(),
|
||||||
|
doDesktopSearch: z.boolean(),
|
||||||
|
doMobileSearch: z.boolean(),
|
||||||
|
doDailyCheckIn: z.boolean(),
|
||||||
|
doReadToEarn: z.boolean()
|
||||||
|
}),
|
||||||
|
searchOnBingLocalQueries: z.boolean(),
|
||||||
|
globalTimeout: NumberOrString,
|
||||||
|
searchSettings: z.object({
|
||||||
|
scrollRandomResults: z.boolean(),
|
||||||
|
clickRandomResults: z.boolean(),
|
||||||
|
parallelSearching: z.boolean(),
|
||||||
|
queryEngines: z.array(QueryEngineSchema),
|
||||||
|
searchResultVisitTime: NumberOrString,
|
||||||
|
searchDelay: DelaySchema,
|
||||||
|
readDelay: DelaySchema
|
||||||
|
}),
|
||||||
|
debugLogs: z.boolean(),
|
||||||
|
proxy: z.object({
|
||||||
|
queryEngine: z.boolean()
|
||||||
|
}),
|
||||||
|
consoleLogFilter: LogFilterSchema,
|
||||||
|
webhook: WebhookSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
// Account
|
||||||
|
export const AccountSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
totpSecret: z.string().optional(),
|
||||||
|
recoveryEmail: z.string(),
|
||||||
|
geoLocale: z.string(),
|
||||||
|
langCode: z.string(),
|
||||||
|
proxy: z.object({
|
||||||
|
proxyAxios: z.boolean(),
|
||||||
|
url: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
password: z.string(),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
saveFingerprint: z.object({
|
||||||
|
mobile: z.boolean(),
|
||||||
|
desktop: z.boolean()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export function validateConfig(data: unknown): Config {
|
||||||
|
return ConfigSchema.parse(data) as Config
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateAccounts(data: unknown): Account[] {
|
||||||
|
return z.array(AccountSchema).parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkNodeVersion(): void {
|
||||||
|
try {
|
||||||
|
const requiredVersion = pkg.engines?.node
|
||||||
|
|
||||||
|
if (!requiredVersion) {
|
||||||
|
console.warn('No Node.js version requirement found in package.json "engines" field.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!semver.satisfies(process.version, requiredVersion)) {
|
||||||
|
console.error(`Current Node.js version ${process.version} does not satisfy requirement: ${requiredVersion}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to validate Node.js version:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,8 +40,12 @@
|
|||||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||||
"types": ["node"],
|
"types": [
|
||||||
"typeRoots": ["./node_modules/@types"],
|
"node"
|
||||||
|
],
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
|
// 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. */
|
// "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'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
@@ -65,6 +69,14 @@
|
|||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/accounts.json", "src/config.json", "src/functions/queries.json"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"src/**/*.ts",
|
||||||
|
"src/accounts.json",
|
||||||
|
"src/config.json",
|
||||||
|
"src/functions/bing-search-activity-queries.json",
|
||||||
|
"src/functions/search-queries.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user