30 Commits

Author SHA1 Message Date
TheNetsky
d5fd06d229 1.5.3
Fix issue with pop-up dismissal
2025-04-30 16:20:56 +02:00
TheNetsky
0ddc964878 1.5.2
- Updated packages
- Cleanup
- Improved message dismissal
- Improved login functionality

With help from @LightZirconite
2025-04-24 14:50:22 +02:00
CNOCM
325bf65b30 Update README (#264)
* Update Search.ts

* Update README.md
2025-03-03 10:59:06 +01:00
HMCDAT
6f19bd4b0e Update Dockerfile (#262) 2025-02-28 12:34:47 +01:00
AariaX
caf6a42a38 Make shuffleArray more random? (#254) 2025-02-25 16:14:51 +01:00
TheNetsky
352d47229b Merge branch 'main' of https://github.com/TheNetsky/Microsoft-Rewards-Script 2025-02-24 17:59:28 +01:00
TheNetsky
9a12ee1ec8 Fix geoLocale not being uppercase 2025-02-24 17:59:20 +01:00
Netsky
b630c3ddda Update README.md 2025-02-24 15:52:53 +01:00
Netsky
287e3897da Update README.md 2025-02-24 15:51:08 +01:00
Netsky
fcf6aba446 Bump version 2025-02-24 15:49:45 +01:00
AariaX
1102f2ca94 Google API Update (1.5.1) (#247)
* switch to google internal api

* A bit of tidying up

* A bit more of tidying up

* A bit more of tidying up

* Pre 1.5.1

- Add proxy exclusions
- Update ReadMe
- Update config Interface

---------

Co-authored-by: TheNetsky <56271887+TheNetsky@users.noreply.github.com>
2025-02-24 15:49:19 +01:00
1OSA
82a896e83f added webhooklogexcludefunc (#240) 2025-02-20 23:21:46 +01:00
Netsky
b0bd1f52c4 Update queries.json 2025-02-19 19:32:45 +01:00
Netsky
7e4121e01b Update run_daily.sh 2025-02-19 15:29:51 +01:00
Netsky
849406c44f 1.5 inital (#234)
* 1.4.12

* Update README.md

* Update package.json

* Update package.json

* 1.5 initial

- Added parallel mode (experimental, likely no Docker supported)
- Added chalk for clearer logging
- Added support for "SearchOnBing" Activities
- Added more configurable options for certain things
- Redone some of the popup/banner clicking for searching (Redo the entire "popup" clicking, so they're more specifically targeted)
- Axios proxy is now optional in the config
- Fingerprint saving is now optional for desktop and mobile

There needs to be many changes for Docker support, including parallel, the new config settings and general testing!

This is still highly experimental, if you use Docker or want something more stable, use the version before this commit!

* Add queries.json to build

* fix(Login): update URL within authorization loop to reflect current page (#210)

* Many changes

- Updated Packages
- Fixed mobile searches erroring out for dashboard  data
- Reworked "bad page" detection
- Catching more errors
- Reworked the search and "close tabs"
- More fixes to the login
- Fixed to paralell and clustering, thanks to @AariaX

* Docker 1.5 preliminary support (#211)

* Basic docker functionality for 1.5

Preliminary docker support for 1.5. Requires headless=true, clusters=1

* Tidy up timezone, add TZ to compose file

Minor changes that should improve timezone handling, and (hopefully) improve scheduling function

* updated readme to simplify and clarify docker instructions

also removed env vars from table

* Fix syntax for cron

* Fix scheduling, add .gitattributes to normalize line endings

fixed line endings caused by Windows in crontab.template and run_daily.sh, which were breaking cron and script execution in the Docker container.

* Removed unnecessary scheduling key from config.json

This key isn't necessary for docker or the base script.

* Basic docker functionality for 1.5

Preliminary docker support for 1.5. Requires headless=true, clusters=1

Tidied up timezone, add TZ to compose file

Minor changes that should improve timezone handling, and (hopefully) improve scheduling function

updated readme to simplify and clarify docker instructions

also removed env vars from table

Fixed syntax for cron

Fixed scheduling, add .gitattributes to normalize line endings

Fixed line endings caused by Windows in crontab.template and run_daily.sh, which were breaking cron and script execution in the Docker container.

Removed unnecessary scheduling key from config.json

This key isn't necessary for docker or the base script.

* Improve scheduling handling, show logs in console

Fixes scheduling when RUN_ON_START=true, and fixes scheduled runs not appearing in docker logs.

* Update compose.yaml

revert service and container name, revert volumes for better generalization, add tips to environment to set scheduling, timezone and whether to run on container start

* Update README.md

proper container name

Co-authored-by: AariaX <196196201+AariaX@users.noreply.github.com>

---------

Co-authored-by: AariaX <196196201+AariaX@users.noreply.github.com>

* Fixes

- Reworked some of the point counting
- Reverted back to the "playwright" package
- Fixed error throw for emailPrefill

* Update config.json

* Add pre-build script

* Update package.json

* Handle 2FA in parallel mode (#219)

* catch error in reloadBadPage (#220)

* Use pre-build and simplify dockerfile (#218)

This uses the new pre-build script included in package.json to handle deps greatly simplifying the dockerfile.

* Small improvements

* Small fixes

- Fixed log spam for "Waiting for authorization"
- Increased wait from 2 to 5 seconds
- Increased search to "safer" values for default

* Experimenting with selectors

Seeing #223 I want to try if this is a good new addition, since for most user this SHOULD work just as good as clicking the entire box.

* More stuff

- Added ability to exclude logs by their function name
- Now caching config settings

* fix: don't retry on 0 (#228)

* Improvements

- Check if searches for mobile are enabled before creating the new page in the browser
- Return message if mobile search data cannot be found
- Added more selectors for coupons

* Improve Popup Dismissal

- Now executes in Parallel
- Respects a timeout of 1 second

---------

Co-authored-by: AariaX <196196201+AariaX@users.noreply.github.com>
Co-authored-by: mgrimace <55518507+mgrimace@users.noreply.github.com>
2025-02-15 16:14:47 +01:00
Netsky
d1f4364e18 Change queries format 2024-10-29 15:14:07 +01:00
Netsky
cf79467a4e Update queries.json 2024-10-29 14:03:22 +01:00
Netsky
ae554a0639 Update Queries 2024-10-29 13:56:00 +01:00
Netsky
1a6f9f4ac3 Update queries
Testing stuff
2024-10-28 14:32:36 +01:00
Netsky
45083ed41f Queries for "Search on Bing" activity 2024-10-27 18:02:29 +01:00
mgrimace
8b489d50f7 Dockerfile refactoring (#167)
* Docker updates and maintenance

- Update run_daily.sh to use compose.yaml env var overrides if present on scheduled jobs, not just first run. Defaults back to config.json values if no override specified.
- update sample compose.yaml with volume mapping to keep config.json, accounts.json, and sessions folder persistent on local machine (e.g., to keep data persistent through updates).

* Refactored dockerfile

Combining various steps to reduce layers. Should improve build time and image size.
2024-10-17 21:46:54 +02:00
mgrimace
7151f2351a Docker updates and maintenance (#164)
- Update run_daily.sh to use compose.yaml env var overrides if present on scheduled jobs, not just first run. Defaults back to config.json values if no override specified.
- update sample compose.yaml with volume mapping to keep config.json, accounts.json, and sessions folder persistent on local machine (e.g., to keep data persistent through updates).
2024-10-14 15:21:36 +02:00
Netsky
5afd8cbe1d Update package.json 2024-10-03 15:01:32 +02:00
Netsky
f015649d16 Fix typo 2024-10-02 18:42:01 +02:00
Netsky
034359019c Update README.md 2024-10-02 18:41:06 +02:00
HMCDAT
09ddbee45a 1.4.11 (#146)
* fix misspelling

* fix: avoid skipping eligible items in "More Promotion"

* remove unnecessary output

* Fix Formatting

* replace checking variable

* decrease id length

* revert previous commit

* change condition for eligible promo

* Other fixes

---------

Co-authored-by: TheNetsky <56271887+TheNetsky@users.noreply.github.com>
2024-10-02 18:37:06 +02:00
MuhammaD Usama
7939196e88 Update Dockerfile (#152)
Update Dockerfile for build
2024-09-22 19:46:10 +02:00
TheNetsky
5bc66c5fc9 1.4.10
- Fixed searches for some users
2024-09-02 12:13:38 +02:00
Netsky
c70d6f9cb1 1.4.9 2024-08-30 13:54:35 +02:00
HMCDAT
a47b86e74d Support new quest to get points (#138)
* support passwordless auth (using Authenticator app)

* update readme

* feat: added mobile app tasks (daily check in + read to earn)

* fix some stuff

* make ReadToEarn use the delay config

* fix daily reward search per week

* reorder mobile tasks

* fix message

* Search fixes, reformatting and types

---------

Co-authored-by: TheNetsky <56271887+TheNetsky@users.noreply.github.com>
2024-08-30 13:26:52 +02:00
40 changed files with 1882 additions and 624 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.sh text eol=lf
*.template text eol=lf

View File

@@ -4,14 +4,11 @@ FROM node:18
# Set the working directory in the container # Set the working directory in the container
WORKDIR /usr/src/microsoft-rewards-script WORKDIR /usr/src/microsoft-rewards-script
# Install jq, cron, and gettext-base # Install necessary dependencies for Playwright and cron
RUN apt-get update && apt-get install -y jq cron gettext-base RUN apt-get update && apt-get install -y \
jq \
# Copy all files to the working directory cron \
COPY . . gettext-base \
# Install dependencies including Playwright
RUN apt-get install -y \
xvfb \ xvfb \
libgbm-dev \ libgbm-dev \
libnss3 \ libnss3 \
@@ -19,16 +16,18 @@ RUN apt-get install -y \
libxss1 \ libxss1 \
libatk-bridge2.0-0 \ libatk-bridge2.0-0 \
libgtk-3-0 \ libgtk-3-0 \
tzdata \
wget \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install application dependencies # Copy all files to the working directory
RUN npm install COPY . .
# Build the script # Install dependencies, set permissions, and build the script
RUN npm run build RUN npm install && \
chmod -R 755 /usr/src/microsoft-rewards-script/node_modules && \
# Install playwright chromium npm run pre-build && \
RUN npx playwright install chromium npm run build
# Copy cron file to cron directory # Copy cron file to cron directory
COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
@@ -37,4 +36,12 @@ COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
RUN touch /var/log/cron.log RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally # Define the command to run your application with cron optionally
CMD sh -c 'node src/updateConfig.js && echo "$TZ" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata && if [ "$RUN_ON_START" = "true" ]; then npm start; fi && envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && crontab /etc/cron.d/microsoft-rewards-cron && cron && tail -f /var/log/cron.log' CMD ["sh", "-c", "echo \"$TZ\" > /etc/timezone && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && \
chmod 0644 /etc/cron.d/microsoft-rewards-cron && \
crontab /etc/cron.d/microsoft-rewards-cron && \
cron -f & \
([ \"$RUN_ON_START\" = \"true\" ] && npm start) && \
tail -f /var/log/cron.log"]

108
README.md
View File

@@ -16,68 +16,72 @@ Under development, however mainly for personal use!
- If you automate this script, set it to run at least 2 times a day to make sure it picked up all tasks, set `"runOnZeroPoints": false` so it doesn't run when no points are found. - If you automate this script, set it to run at least 2 times a day to make sure it picked up all tasks, set `"runOnZeroPoints": false` so it doesn't run when no points are found.
## Docker (Experimental) ## ## Docker (Experimental) ##
**Note:** If you had previously built and run the script locally, remove the `/node_modules` and `/dist` from your Microsoft-Rewards-Script folder. ### **Before Starting**
1. Download the source code - If you had previously built and run the script locally, **remove** the `/node_modules` and `/dist` folders from your `Microsoft-Rewards-Script` directory.
2. Make changes to your `accounts.json` and `config.json` - If you had used Docker with an older version of the script (e.g., 1.4), **remove** any persistently saved `config.json` and session folders. Old `accounts.json` files can be reused.
3. **Headless mode must be enabled.** You can do this in `config.json` or by using the `HEADLESS=true` environmental variable in docker run or docker compose.yaml (see below). Environmental variables are prioritized over the values in config.json.
4. The container has in-built scheduling. Customize your schedule using the `CRON_START_TIME` environmental variable. Use [crontab.guru](crontab.guru) if you're unsure how to create a cron schedule.
5. **Note:** the container will add between 5 and 50 minutes of randomized variability to your scheduled start times.
### Option 1: build and run with docker run ### **Setup the Source Files**
1. Build or re-build the container image with: `docker build -t microsoft-rewards-script-docker .` 1. **Download the Source Code**
2. Run the container with: 2. **Update `accounts.json`**
```bash 3. **Edit `config.json`,** ensuring the following values are set (other settings are up to your preference):
docker run --name netsky -d \
-e TZ=America/New_York \ ```json
-e HEADLESS=true \ "headless": true,
-e RUN_ON_START=true \ "clusters": 1,
-e CRON_START_TIME="0 5,11 * * *" \
microsoft-rewards-script-docker
``` ```
3. Optionally, customize your config by adding any other environmental variables from the table below. ### **Customize the `compose.yaml` File**
4. You can view logs with `docker logs netsky`. A basic docker `compose.yaml` is provided. Follow these steps to configure and run the container:
### Option 2: use docker compose 1. **Set Your Timezone:** Adjust the `TZ` variable to ensure correct scheduling.
2. **Configure Persistent Storage:**
1. A basic docker compose.yaml has been provided. - Map `config.json` and `accounts.json` to retain settings and accounts.
- (Optional) Use a persistent `sessions` folder to save login sessions.
2. Optionally, customize your config by adding any other environmental variables from the table below. 3. **Customize the Schedule:**
- Modify `CRON_SCHEDULE` to set run times. Use [crontab.guru](https://crontab.guru) for help.
3. Build and start the container using `docker compose up -d`. - **Note:** The container adds 550 minutes of random variability to each scheduled start time.
4. **(Optional) Run on Startup:**
4. You can view logs with `docker logs netsky` - Set `RUN_ON_START=true` to execute the script immediately when the container starts.
5. **Start the Container:** Run `docker compose up -d` to build and launch.
6. **Monitor Logs:** Use `docker logs microsoft-rewards-script` to view script execution and to retrieve 'passwordless' login codes.
## Config ## ## Config ##
| Setting | Description | Default | Docker Environmental Variable | | Setting | Description | Default |
| :------------- |:-------------| :-----| :-----| | :------------- |:-------------| :-----|
| baseURL | MS Rewards page | `https://rewards.bing.com` | BASE_URL | | baseURL | MS Rewards page | `https://rewards.bing.com` |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) | SESSION_PATH | | sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) | HEADLESS *(must be set to `=true` for docker)* | | headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | RUN_ON_ZERO_POINTS | | parallel | If you want mobile and desktop tasks to run parallel or sequential| `true` |
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) | CLUSTERS | | runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) |
| saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | SAVE_FINGERPRINT | | clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
| workers.doDailySet | Complete daily set items | `true` | DO_DAILY_SET | | saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
| workers.doMorePromotions | Complete promotional items | `true` | DO_MORE_PROMOTIONS | | saveFingerprint.desktop | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
| workers.doPunchCards | Complete punchcards | `true` | DO_PUNCH_CARDS | | workers.doDailySet | Complete daily set items | `true` |
| workers.doDesktopSearch | Complete daily desktop searches | `true` | DO_DESKTOP_SEARCH | | workers.doMorePromotions | Complete promotional items | `true` |
| workers.doMobileSearch | Complete daily mobile searches | `true` | DO_MOBILE_SEARCH | | workers.doPunchCards | Complete punchcards | `true` |
| globalTimeout | The length before the action gets timeout | `30000` (30 seconds) | GLOBAL_TIMEOUT | | workers.doDesktopSearch | Complete daily desktop searches | `true` |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | USE_GEO_LOCALE_QUERIES | | workers.doMobileSearch | Complete daily mobile searches | `true` |
| scrollRandomResults | Scroll randomly in search results | `true` | SCROLL_RANDOM_RESULTS | | workers.doDailyCheckIn | Complete daily check-in activity | `true` |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | CLICK_RANDOM_RESULTS | | workers.doReadToEarn | Complete read to earn activity | `true` |
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 10000` (10 seconds) `max: 20000` (20 seconds) | SEARCH_DELAY_MIN SEARCH_DELAY_MAX | | searchOnBingLocalQueries | Complete the activity "search on Bing" using the `queries.json` or fetched from this repo | `false` (Will fetch from this repo) |
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | RETRY_MOBILE_SEARCH | | globalTimeout | The length before the action gets timeout | `30s` |
| webhook.enabled | Enable or disable your set webhook | `false` | WEBHOOK_ENABLED | | searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
| webhook.url | Your Discord webhook URL | `null` | WEBHOOK_URL="" | | searchSettings.scrollRandomResults | Scroll randomly in search results | `true` |
| cronStartTime | Scheduled script run-time, *only available for docker implementation* | `0 5,11 * * *` (5:00 am, 11:00 am daily) | CRON_START_TIME="" | | searchSettings.clickRandomResults | Visit random website from search result| `true` |
| | Run the script immediately when the Docker container starts | `true` | RUN_ON_START | | searchSettings.searchDelay | Minimum and maximum time in milliseconds between search queries | `min: 3min` `max: 5min` |
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `2` |
| logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` |
| webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` |
| proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
| proxy.proxyBingTerms | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
| webhook.enabled | Enable or disable your set webhook | `false` |
| webhook.url | Your Discord webhook URL | `null` |
## Features ## ## Features ##
- [x] Multi-Account Support - [x] Multi-Account Support
@@ -102,8 +106,8 @@ Under development, however mainly for personal use!
- [x] Completing Punchcards - [x] Completing Punchcards
- [x] Solving This Or That Quiz (Random) - [x] Solving This Or That Quiz (Random)
- [x] Solving ABC Quiz - [x] Solving ABC Quiz
- [ ] Completing Shopping Game - [x] Completing Daily Check In
- [ ] Completing Gaming Tab - [x] Completing Read To Earn
- [x] Clustering Support - [x] Clustering Support
- [x] Proxy Support - [x] Proxy Support
- [x] Docker Support (experimental) - [x] Docker Support (experimental)

View File

@@ -1,13 +1,15 @@
services: services:
microsoft-rewards-script: microsoft-rewards-script:
build: . build: .
container_name: netsky container_name: microsoft-rewards-script
environment:
- TZ=America/Toronto #change to your local timezone
- NODE_ENV=production
- HEADLESS=true #do not change
### Customize your run schedule, default 5:00 am and 11:00 am, use crontab.guru for formatting
- CRON_START_TIME=0 5,11 * * *
### Run on start, set to false to only run the script per the cron schedule
- RUN_ON_START=true
restart: unless-stopped restart: unless-stopped
volumes:
### Replace "/path/to/" with the actual path to where you want to save the files on your local machine.
- /path/to/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json
- /path/to/config.json:/usr/src/microsoft-rewards-script/dist/config.json
- /path/to/sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
environment:
- NODE_ENV=production
- CRON_SCHEDULE=0 7,15,20 * * * # Customize your schedule, use crontab.guru for formatting
- RUN_ON_START=true # Runs the script on container startup
- TZ=America/Toronto # Set your timezone for proper scheduling

View File

@@ -1,12 +1,13 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "1.4.8", "version": "1.5.3",
"description": "Automatically do tasks for Microsoft Rewards but in TS!", "description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"pre-build": "npm i && rimraf dist && npx playwright install chromium",
"build": "tsc", "build": "tsc",
"start": "node ./dist/index.js", "start": "node ./dist/index.js",
"ts-start": "ts-node ./src/index.ts", "ts-start": "ts-node ./src/index.ts",
@@ -26,17 +27,25 @@
"author": "Netsky", "author": "Netsky",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/ms": "^0.7.34",
"@typescript-eslint/eslint-plugin": "^7.17.0", "@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
"rimraf": "^6.0.1",
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.4", "axios": "^1.8.4",
"chalk": "^4.1.2",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.54", "fingerprint-generator": "^2.1.66",
"fingerprint-injector": "^2.1.54", "fingerprint-injector": "^2.1.66",
"playwright": "^1.46.1", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"ms": "^2.1.3",
"playwright": "1.47.2",
"rebrowser-playwright": "1.47.2",
"socks-proxy-agent": "^8.0.5",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
} }

View File

@@ -3,6 +3,7 @@
"email": "email_1", "email": "email_1",
"password": "password_1", "password": "password_1",
"proxy": { "proxy": {
"proxyAxios": true,
"url": "", "url": "",
"port": 0, "port": 0,
"username": "", "username": "",
@@ -13,6 +14,7 @@
"email": "email_2", "email": "email_2",
"password": "password_2", "password": "password_2",
"proxy": { "proxy": {
"proxyAxios": true,
"url": "", "url": "",
"port": 0, "port": 0,
"username": "", "username": "",

View File

@@ -1,18 +1,20 @@
import playwright from 'playwright' import playwright, { BrowserContext } from 'rebrowser-playwright'
import { BrowserContext } from 'playwright'
import { newInjectedContext } from 'fingerprint-injector' import { newInjectedContext } from 'fingerprint-injector'
import { FingerprintGenerator } from 'fingerprint-generator' import { FingerprintGenerator } from 'fingerprint-generator'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { loadSessionData, saveFingerprintData } from '../util/Load' import { loadSessionData, saveFingerprintData } from '../util/Load'
import { updateFingerprintUserAgent } from '../util/UserAgent'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../interface/Account'
/* Test Stuff /* Test Stuff
https://abrahamjuliot.github.io/creepjs/ https://abrahamjuliot.github.io/creepjs/
https://botcheck.luminati.io/ https://botcheck.luminati.io/
http://f.vision/ https://fv.pro/
https://pixelscan.net/
https://www.browserscan.net/
*/ */
class Browser { class Browser {
@@ -39,32 +41,34 @@ class Browser {
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint) const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
const fingerpint = sessionData.fingerprint ? sessionData.fingerprint : this.generateFingerprint() const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint()
const context = await newInjectedContext(browser, { fingerprint: fingerpint }) const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
// Set timeout to preferred amount // Set timeout to preferred amount
context.setDefaultTimeout(this.bot.config?.globalTimeout ?? 30_000) context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
await context.addCookies(sessionData.cookies) await context.addCookies(sessionData.cookies)
if (this.bot.config.saveFingerprint) { if (this.bot.config.saveFingerprint) {
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerpint) await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
} }
this.bot.log('BROWSER', `Created browser with User-Agent: "${fingerpint.fingerprint.navigator.userAgent}"`) this.bot.log(this.bot.isMobile, 'BROWSER', `Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`)
return context return context as BrowserContext
} }
generateFingerprint() { async generateFingerprint() {
const fingerPrintData = new FingerprintGenerator().getFingerprint({ const fingerPrintData = new FingerprintGenerator().getFingerprint({
devices: this.bot.isMobile ? ['mobile'] : ['desktop'], devices: this.bot.isMobile ? ['mobile'] : ['desktop'],
operatingSystems: this.bot.isMobile ? ['android'] : ['windows'], operatingSystems: this.bot.isMobile ? ['android'] : ['windows'],
browsers: ['edge'] browsers: [{ name: 'edge' }]
}) })
return fingerPrintData const updatedFingerPrintData = await updateFingerprintUserAgent(fingerPrintData, this.bot.isMobile)
return updatedFingerPrintData
} }
} }

View File

@@ -1,10 +1,14 @@
import { Page } from 'playwright' import { BrowserContext, Page } from 'rebrowser-playwright'
import { CheerioAPI, load } from 'cheerio' import { CheerioAPI, load } from 'cheerio'
import { AxiosRequestConfig } from 'axios'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData' import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData'
import { QuizData } from './../interface/QuizData' import { QuizData } from './../interface/QuizData'
import { AppUserData } from '../interface/AppUserData'
import { EarnablePoints } from '../interface/Points'
export default class BrowserFunc { export default class BrowserFunc {
@@ -34,19 +38,19 @@ export default class BrowserFunc {
for (let iteration = 1; iteration <= maxIterations; iteration++) { for (let iteration = 1; iteration <= maxIterations; iteration++) {
await this.bot.utils.wait(3000) await this.bot.utils.wait(3000)
await this.bot.browser.utils.tryDismissCookieBanner(page) await this.bot.browser.utils.tryDismissAllMessages(page)
// Check if account is suspended // Check if account is suspended
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false) const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (isSuspended) { if (isSuspended) {
this.bot.log('GO-HOME', 'This account is suspended!', 'error') this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account is suspended!', 'error')
throw new Error('Account has been suspended!') throw new Error('Account has been suspended!')
} }
try { try {
// If activities are found, exit the loop // If activities are found, exit the loop
await page.waitForSelector('#more-activities', { timeout: 1000 }) await page.waitForSelector('#more-activities', { timeout: 1000 })
this.bot.log('GO-HOME', 'Visited homepage successfully') this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
break break
} catch (error) { } catch (error) {
@@ -62,7 +66,7 @@ export default class BrowserFunc {
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.goto(this.bot.config.baseURL) await page.goto(this.bot.config.baseURL)
} else { } else {
this.bot.log('GO-HOME', 'Visited homepage successfully') this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
break break
} }
@@ -70,7 +74,7 @@ export default class BrowserFunc {
} }
} catch (error) { } catch (error) {
throw this.bot.log('GO-HOME', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred:' + error, 'error')
} }
} }
@@ -82,42 +86,49 @@ export default class BrowserFunc {
const dashboardURL = new URL(this.bot.config.baseURL) const dashboardURL = new URL(this.bot.config.baseURL)
const currentURL = new URL(this.bot.homePage.url()) const currentURL = new URL(this.bot.homePage.url())
// Should never happen since tasks are opened in a new tab! try {
if (currentURL.hostname !== dashboardURL.hostname) { // Should never happen since tasks are opened in a new tab!
this.bot.log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page') if (currentURL.hostname !== dashboardURL.hostname) {
await this.goHome(this.bot.homePage) this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
} await this.goHome(this.bot.homePage)
// Reload the page to get new data
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
const scriptContent = await this.bot.homePage.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
return targetScript?.innerText ? targetScript.innerText : null
})
if (!scriptContent) {
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
}
// Extract the dashboard object from the script content
const dashboardData = await this.bot.homePage.evaluate(scriptContent => {
// Extract the dashboard object using regex
const regex = /var dashboard = (\{.*?\});/s
const match = regex.exec(scriptContent)
if (match && match[1]) {
return JSON.parse(match[1])
} }
}, scriptContent)
if (!dashboardData) { // Reload the page to get new data
throw this.bot.log('GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error') await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
const scriptContent = await this.bot.homePage.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
return targetScript?.innerText ? targetScript.innerText : null
})
if (!scriptContent) {
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
}
// Extract the dashboard object from the script content
const dashboardData = await this.bot.homePage.evaluate(scriptContent => {
// Extract the dashboard object using regex
const regex = /var dashboard = (\{.*?\});/s
const match = regex.exec(scriptContent)
if (match && match[1]) {
return JSON.parse(match[1])
}
}, scriptContent)
if (!dashboardData) {
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
}
return dashboardData
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Error fetching dashboard data: ${error}`, 'error')
} }
return dashboardData
} }
/** /**
@@ -131,42 +142,109 @@ export default class BrowserFunc {
} }
/** /**
* Get total earnable points * Get total earnable points with web browser
* @returns {number} Total earnable points * @returns {number} Total earnable points
*/ */
async getEarnablePoints(): Promise<number> { async getBrowserEarnablePoints(): Promise<EarnablePoints> {
try { try {
const data = await this.getDashboardData() let desktopSearchPoints = 0
let mobileSearchPoints = 0
let dailySetPoints = 0
let morePromotionsPoints = 0
// These only include the points from tasks that the script can complete! const data = await this.getDashboardData()
let totalEarnablePoints = 0
// Desktop Search Points // Desktop Search Points
if (data.userStatus.counters.pcSearch?.length) { if (data.userStatus.counters.pcSearch?.length) {
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) data.userStatus.counters.pcSearch.forEach(x => desktopSearchPoints += (x.pointProgressMax - x.pointProgress))
} }
// Mobile Search Points // Mobile Search Points
if (data.userStatus.counters.mobileSearch?.length) { if (data.userStatus.counters.mobileSearch?.length) {
data.userStatus.counters.mobileSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) data.userStatus.counters.mobileSearch.forEach(x => mobileSearchPoints += (x.pointProgressMax - x.pointProgress))
} }
// Daily Set // Daily Set
data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => dailySetPoints += (x.pointProgressMax - x.pointProgress))
// More Promotions // More Promotions
if (data.morePromotions?.length) { if (data.morePromotions?.length) {
data.morePromotions.forEach(x => { data.morePromotions.forEach(x => {
// Only count points from supported activities // Only count points from supported activities
if (['quiz', 'urlreward'].includes(x.promotionType) && !x.attributes.is_unlocked) { if (['quiz', 'urlreward'].includes(x.promotionType) && x.exclusiveLockedFeatureStatus !== 'locked') {
totalEarnablePoints += (x.pointProgressMax - x.pointProgress) morePromotionsPoints += (x.pointProgressMax - x.pointProgress)
} }
}) })
} }
return totalEarnablePoints const totalEarnablePoints = desktopSearchPoints + mobileSearchPoints + dailySetPoints + morePromotionsPoints
return {
dailySetPoints,
morePromotionsPoints,
desktopSearchPoints,
mobileSearchPoints,
totalEarnablePoints
}
} catch (error) { } catch (error) {
throw this.bot.log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
}
}
/**
* Get total earnable points with mobile app
* @returns {number} Total earnable points
*/
async getAppEarnablePoints(accessToken: string) {
try {
const points = {
readToEarn: 0,
checkIn: 0,
totalEarnablePoints: 0
}
const eligibleOffers = [
'ENUS_readarticle3_30points',
'Gamification_Sapphire_DailyCheckIn'
]
const data = await this.getDashboardData()
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
const userDataRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
}
}
const userDataResponse: AppUserData = (await this.bot.axios.request(userDataRequest)).data
const userData = userDataResponse.response
const eligibleActivities = userData.promotions.filter((x) => eligibleOffers.includes(x.attributes.offerid ?? ''))
for (const item of eligibleActivities) {
if (item.attributes.type === 'msnreadearn') {
points.readToEarn = parseInt(item.attributes.pointmax ?? '') - parseInt(item.attributes.pointprogress ?? '')
break
} else if (item.attributes.type === 'checkin') {
const checkInDay = parseInt(item.attributes.progress ?? '') % 7
if (checkInDay < 6 && (new Date()).getDate() != (new Date(item.attributes.last_updated ?? '')).getDate()) {
points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '')
}
break
}
}
points.totalEarnablePoints = points.readToEarn + points.checkIn
return points
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
} }
} }
@@ -180,7 +258,7 @@ export default class BrowserFunc {
return data.userStatus.availablePoints return data.userStatus.availablePoints
} catch (error) { } catch (error) {
throw this.bot.log('GET-CURRENT-POINTS', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', 'An error occurred:' + error, 'error')
} }
} }
@@ -206,26 +284,26 @@ export default class BrowserFunc {
const quizData = JSON.parse(match[1]) const quizData = JSON.parse(match[1])
return quizData return quizData
} else { } else {
throw this.bot.log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error') throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Quiz data not found within script', 'error')
} }
} else { } else {
throw this.bot.log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error') throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
} }
} catch (error) { } catch (error) {
throw this.bot.log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'An error occurred:' + error, 'error')
} }
} }
async waitForQuizRefresh(page: Page): Promise<boolean> { async waitForQuizRefresh(page: Page): Promise<boolean> {
try { try {
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
return true return true
} catch (error) { } catch (error) {
this.bot.log('QUIZ-REFRESH', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'An error occurred:' + error, 'error')
return false return false
} }
} }
@@ -241,7 +319,7 @@ export default class BrowserFunc {
} }
} }
async refreshCheerio(page: Page): Promise<CheerioAPI> { async loadInCheerio(page: Page): Promise<CheerioAPI> {
const html = await page.content() const html = await page.content()
const $ = load(html) const $ = load(html)
@@ -259,10 +337,24 @@ export default class BrowserFunc {
selector = `a[href*="${element.attribs.href}"]` selector = `a[href*="${element.attribs.href}"]`
} }
} catch (error) { } catch (error) {
this.bot.log('GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error')
} }
return selector return selector
} }
async closeBrowser(browser: BrowserContext, email: string) {
try {
// Save cookies
await saveSessionData(this.bot.config.sessionPath, browser, email, this.bot.isMobile)
await this.bot.utils.wait(2000)
// Close browser
await browser.close()
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'An error occurred:' + error, 'error')
}
}
} }

View File

@@ -1,4 +1,5 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { load } from 'cheerio'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
@@ -10,69 +11,41 @@ export default class BrowserUtil {
this.bot = bot this.bot = bot
} }
async tryDismissAllMessages(page: Page): Promise<boolean> { async tryDismissAllMessages(page: Page): Promise<void> {
const buttons = [ const buttons = [
{ selector: 'button[type="submit"]', label: 'Submit Button' },
{ selector: '#acceptButton', label: 'AcceptButton' }, { selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' }, { selector: '#iLandingViewAction', label: 'iLandingViewAction' },
{ selector: '#iShowSkip', label: 'iShowSkip' }, { selector: '#iShowSkip', label: 'iShowSkip' },
{ selector: '#iNext', label: 'iNext' }, { selector: '#iNext', label: 'iNext' },
{ selector: '#iLooksGood', label: 'iLooksGood' }, { selector: '#iLooksGood', label: 'iLooksGood' },
{ selector: '#idSIButton9', label: 'idSIButton9' }, { selector: '#idSIButton9', label: 'idSIButton9' },
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' } { selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' },
{ selector: '.maybe-later', label: 'Mobile Rewards App Banner' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container', isXPath: true },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
] ]
let result = false
for (const button of buttons) { for (const button of buttons) {
try { try {
const element = await page.waitForSelector(button.selector, { state: 'visible', timeout: 1000 }) const element = button.isXPath ? page.locator(`xpath=${button.selector}`) : page.locator(button.selector)
if (element) { await element.first().click({ timeout: 500 })
await element.click() await page.waitForTimeout(500)
result = true
} this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
} catch (error) { } catch (error) {
continue // Silent fail
} }
} }
return result
}
async tryDismissCookieBanner(page: Page): Promise<void> {
try {
await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 })
const cookieBanner = await page.$('#cookieConsentContainer')
if (cookieBanner) {
const button = await cookieBanner.$('button')
if (button) {
await button.click()
await this.bot.utils.wait(2000)
}
}
} catch (error) {
// Continue if element is not found or other error occurs
}
}
async tryDismissBingCookieBanner(page: Page): Promise<void> {
try {
await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 })
const cookieBanner = await page.$('#bnp_btn_accept')
if (cookieBanner) {
await cookieBanner.click()
}
} catch (error) {
// Continue if element is not found or other error occurs
}
} }
async getLatestTab(page: Page): Promise<Page> { async getLatestTab(page: Page): Promise<Page> {
try { try {
await this.bot.utils.wait(500) await this.bot.utils.wait(1000)
const browser = page.context() const browser = page.context()
const pages = browser.pages() const pages = browser.pages()
@@ -82,9 +55,9 @@ export default class BrowserUtil {
return newTab return newTab
} }
throw this.bot.log('GET-NEW-TAB', 'Unable to get latest tab', 'error') throw this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Unable to get latest tab', 'error')
} catch (error) { } catch (error) {
throw this.bot.log('GET-NEW-TAB', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'An error occurred:' + error, 'error')
} }
} }
@@ -97,19 +70,19 @@ export default class BrowserUtil {
let homeTabURL: URL let homeTabURL: URL
if (!homeTab) { if (!homeTab) {
throw this.bot.log('GET-TABS', 'Home tab could not be found!', 'error') throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Home tab could not be found!', 'error')
} else { } else {
homeTabURL = new URL(homeTab.url()) homeTabURL = new URL(homeTab.url())
if (homeTabURL.hostname !== 'rewards.bing.com') { if (homeTabURL.hostname !== 'rewards.bing.com') {
throw this.bot.log('GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error') throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
} }
} }
const workerTab = pages[2] const workerTab = pages[2]
if (!workerTab) { if (!workerTab) {
throw this.bot.log('GET-TABS', 'Worker tab could not be found!', 'error') throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Worker tab could not be found!', 'error')
} }
return { return {
@@ -118,7 +91,24 @@ export default class BrowserUtil {
} }
} catch (error) { } catch (error) {
throw this.bot.log('GET-TABS', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'An error occurred:' + error, 'error')
}
}
async reloadBadPage(page: Page): Promise<void> {
try {
const html = await page.content().catch(() => '')
const $ = load(html)
const isNetworkError = $('body.neterror').length
if (isNetworkError) {
this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', 'Bad page detected, reloading!')
await page.reload()
}
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -2,30 +2,46 @@
"baseURL": "https://rewards.bing.com", "baseURL": "https://rewards.bing.com",
"sessionPath": "sessions", "sessionPath": "sessions",
"headless": false, "headless": false,
"parallel": true,
"runOnZeroPoints": false, "runOnZeroPoints": false,
"clusters": 1, "clusters": 1,
"saveFingerprint": false, "saveFingerprint": {
"mobile": false,
"desktop": false
},
"workers": { "workers": {
"doDailySet": true, "doDailySet": true,
"doMorePromotions": true, "doMorePromotions": true,
"doPunchCards": true, "doPunchCards": true,
"doDesktopSearch": true, "doDesktopSearch": true,
"doMobileSearch": true "doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true
}, },
"globalTimeout": 30000, "searchOnBingLocalQueries": false,
"globalTimeout": "30s",
"searchSettings": { "searchSettings": {
"useGeoLocaleQueries": false, "useGeoLocaleQueries": false,
"scrollRandomResults": true, "scrollRandomResults": true,
"clickRandomResults": true, "clickRandomResults": true,
"searchDelay": { "searchDelay": {
"min": 10000, "min": "3min",
"max": 20000 "max": "5min"
}, },
"retryMobileSearch": true "retryMobileSearchAmount": 2
},
"logExcludeFunc": [
"SEARCH-CLOSE-TABS"
],
"webhookLogExcludeFunc": [
"SEARCH-CLOSE-TABS"
],
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
}, },
"webhook": { "webhook": {
"enabled": false, "enabled": false,
"url": "" "url": ""
}, }
"cronStartTime": "0 5,11 * * *"
} }

View File

@@ -1 +1 @@
${CRON_START_TIME} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /var/log/cron.log 2>&1 ${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /proc/1/fd/1 2>> /proc/1/fd/2

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
@@ -8,8 +8,11 @@ import { Poll } from './activities/Poll'
import { Quiz } from './activities/Quiz' import { Quiz } from './activities/Quiz'
import { ThisOrThat } from './activities/ThisOrThat' import { ThisOrThat } from './activities/ThisOrThat'
import { UrlReward } from './activities/UrlReward' import { UrlReward } from './activities/UrlReward'
import { SearchOnBing } from './activities/SearchOnBing'
import { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData } from '../interface/DashboardData' import { DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
export default class Activities { export default class Activities {
@@ -49,4 +52,19 @@ export default class Activities {
await urlReward.doUrlReward(page) await urlReward.doUrlReward(page)
} }
doSearchOnBing = async (page: Page, activity: MorePromotion | PromotionalItem): Promise<void> => {
const searchOnBing = new SearchOnBing(this.bot)
await searchOnBing.doSearchOnBing(page, activity)
}
doReadToEarn = async (accessToken: string, data: DashboardData): Promise<void> => {
const readToEarn = new ReadToEarn(this.bot)
await readToEarn.doReadToEarn(accessToken, data)
}
doDailyCheckIn = async (accessToken: string, data: DashboardData): Promise<void> => {
const dailyCheckIn = new DailyCheckIn(this.bot)
await dailyCheckIn.doDailyCheckIn(accessToken, data)
}
} }

View File

@@ -1,17 +1,26 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import readline from 'readline' import readline from 'readline'
import * as crypto from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load' import { saveSessionData } from '../util/Load'
import { OAuth } from '../interface/OAuth'
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}) })
export class Login { export class Login {
private bot: MicrosoftRewardsBot private bot: MicrosoftRewardsBot
private clientId: string = '0000000040170455'
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
constructor(bot: MicrosoftRewardsBot) { constructor(bot: MicrosoftRewardsBot) {
this.bot = bot this.bot = bot
@@ -23,20 +32,23 @@ export class Login {
// Navigate to the Bing login page // Navigate to the Bing login page
await page.goto('https://rewards.bing.com/signin') await page.goto('https://rewards.bing.com/signin')
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false) await page.waitForLoadState('domcontentloaded').catch(() => { })
await this.bot.browser.utils.reloadBadPage(page)
// Check if account is locked
await this.checkAccountLocked(page)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
if (!isLoggedIn) { if (!isLoggedIn) {
// Check if account is locked
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
if (isLocked) {
this.bot.log('LOGIN', 'This account has been locked!', 'error')
throw new Error('Account has been locked!')
}
await this.execLogin(page, email, password) await this.execLogin(page, email, password)
this.bot.log('LOGIN', 'Logged into Microsoft successfully') this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft successfully')
} else { } else {
this.bot.log('LOGIN', 'Already logged in') this.bot.log(this.bot.isMobile, 'LOGIN', 'Already logged in')
// Check if account is locked
await this.checkAccountLocked(page)
} }
// Check if logged in to bing // Check if logged in to bing
@@ -46,43 +58,108 @@ export class Login {
await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile) await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile)
// We're done logging in // We're done logging in
this.bot.log('LOGIN', 'Logged in successfully') this.bot.log(this.bot.isMobile, 'LOGIN', 'Logged in successfully, saved login session!')
} catch (error) { } catch (error) {
// Throw and don't continue // Throw and don't continue
throw this.bot.log('LOGIN', 'An error occurred:' + error, 'error') throw this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred:' + error, 'error')
} }
} }
private async execLogin(page: Page, email: string, password: string) { private async execLogin(page: Page, email: string, password: string) {
try { try {
await this.enterEmail(page, email) await this.enterEmail(page, email)
await this.bot.utils.wait(2000)
await this.bot.browser.utils.reloadBadPage(page)
await this.bot.utils.wait(2000)
await this.enterPassword(page, password) await this.enterPassword(page, password)
await this.bot.utils.wait(2000)
// Check if account is locked
await this.checkAccountLocked(page)
await this.bot.browser.utils.reloadBadPage(page)
await this.checkLoggedIn(page) await this.checkLoggedIn(page)
} catch (error: any) { } catch (error) {
this.bot.log('LOGIN', 'An error occurred: ' + error.message, 'error') this.bot.log(this.bot.isMobile, 'LOGIN', 'An error occurred: ' + error, 'error')
} }
} }
private async enterEmail(page: Page, email: string) { private async enterEmail(page: Page, email: string) {
await page.fill('#i0116', email) const emailInputSelector = 'input[type="email"]'
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully') try {
// Wait for email field
const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).catch(() => null)
if (!emailField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
return
}
await this.bot.utils.wait(1000)
// Check if email is prefilled
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 5000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
} else {
// Else clear and fill email
await page.fill(emailInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(emailInputSelector, email)
await this.bot.utils.wait(1000)
}
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
}
} }
private async enterPassword(page: Page, password: string) { private async enterPassword(page: Page, password: string) {
const passwordInputSelector = 'input[type="password"]'
try { try {
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) // Wait for password field
await this.bot.utils.wait(2000) const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
await page.fill('#i0118', password) if (!passwordField) {
await page.click('#idSIButton9') this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
this.bot.log('LOGIN', 'Password entered successfully') await this.handle2FA(page)
} catch { return
this.bot.log('LOGIN', 'Password entry failed or 2FA required') }
await this.bot.utils.wait(1000)
// Clear and fill password
await page.fill(passwordInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(passwordInputSelector, password)
await this.bot.utils.wait(1000)
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
await this.handle2FA(page) await this.handle2FA(page)
} }
} }
private async handle2FA(page: Page) { private async handle2FA(page: Page) {
try { try {
const numberToPress = await this.get2FACode(page) const numberToPress = await this.get2FACode(page)
@@ -93,8 +170,8 @@ export class Login {
// SMS verification // SMS verification
await this.authSMSVerification(page) await this.authSMSVerification(page)
} }
} catch (error: any) { } catch (error) {
this.bot.log('LOGIN', `2FA handling failed: ${error.message}`) this.bot.log(this.bot.isMobile, 'LOGIN', `2FA handling failed: ${error}`)
} }
} }
@@ -103,7 +180,25 @@ export class Login {
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent() return await element.textContent()
} catch { } catch {
await page.click('button[aria-describedby="confirmSendTitle"]') if (this.bot.config.parallel) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Script running in parallel, can only send 1 2FA request per account at a time!', 'log', 'yellow')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Trying again in 60 seconds! Please wait...', 'log', 'yellow')
// eslint-disable-next-line no-constant-condition
while (true) {
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
if (button) {
await this.bot.utils.wait(60000)
await button.click()
continue
} else {
break
}
}
}
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent() return await element.textContent()
@@ -114,15 +209,15 @@ export class Login {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
try { try {
this.bot.log('LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`) this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
this.bot.log('LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds') this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 }) await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
this.bot.log('LOGIN', 'Login successfully approved!') this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
break break
} catch { } catch {
this.bot.log('LOGIN', 'The code is expired. Trying to get a new code...') this.bot.log(this.bot.isMobile, 'LOGIN', 'The code is expired. Trying to get a new code...')
await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]') await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]')
numberToPress = await this.get2FACode(page) numberToPress = await this.get2FACode(page)
} }
@@ -130,7 +225,7 @@ export class Login {
} }
private async authSMSVerification(page: Page) { private async authSMSVerification(page: Page) {
this.bot.log('LOGIN', 'SMS 2FA code required. Waiting for user input...') this.bot.log(this.bot.isMobile, 'LOGIN', 'SMS 2FA code required. Waiting for user input...')
const code = await new Promise<string>((resolve) => { const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => { rl.question('Enter 2FA code:\n', (input) => {
@@ -141,7 +236,7 @@ export class Login {
await page.fill('input[name="otc"]', code) await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
this.bot.log('LOGIN', '2FA code entered successfully') this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code entered successfully')
} }
private async checkLoggedIn(page: Page) { private async checkLoggedIn(page: Page) {
@@ -158,13 +253,13 @@ export class Login {
} }
// Wait for login to complete // Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }) await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
this.bot.log('LOGIN', 'Successfully logged into the rewards portal') this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal')
} }
private async checkBingLogin(page: Page): Promise<void> { private async checkBingLogin(page: Page): Promise<void> {
try { try {
this.bot.log('LOGIN-BING', 'Verifying Bing login') this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing login')
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F') await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F')
const maxIterations = 5 const maxIterations = 5
@@ -173,12 +268,12 @@ export class Login {
const currentUrl = new URL(page.url()) const currentUrl = new URL(page.url())
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') { if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
await this.bot.browser.utils.tryDismissBingCookieBanner(page) await this.bot.browser.utils.tryDismissAllMessages(page)
const loggedIn = await this.checkBingLoginStatus(page) const loggedIn = await this.checkBingLoginStatus(page)
// If mobile browser, skip this step // If mobile browser, skip this step
if (loggedIn || this.bot.isMobile) { if (loggedIn || this.bot.isMobile) {
this.bot.log('LOGIN-BING', 'Bing login verification passed!') this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing login verification passed!')
break break
} }
} }
@@ -187,7 +282,7 @@ export class Login {
} }
} catch (error) { } catch (error) {
this.bot.log('LOGIN-BING', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'An error occurred:' + error, 'error')
} }
} }
@@ -200,4 +295,61 @@ export class Login {
} }
} }
async getMobileAccessToken(page: Page, email: string) {
const authorizeUrl = new URL(this.authBaseUrl)
authorizeUrl.searchParams.append('response_type', 'code')
authorizeUrl.searchParams.append('client_id', this.clientId)
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
authorizeUrl.searchParams.append('scope', this.scope)
authorizeUrl.searchParams.append('state', crypto.randomBytes(16).toString('hex'))
authorizeUrl.searchParams.append('access_type', 'offline_access')
authorizeUrl.searchParams.append('login_hint', email)
await page.goto(authorizeUrl.href)
let currentUrl = new URL(page.url())
let code: string
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Waiting for authorization...')
// eslint-disable-next-line no-constant-condition
while (true) {
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
code = currentUrl.searchParams.get('code')!
break
}
currentUrl = new URL(page.url())
await this.bot.utils.wait(5000)
}
const body = new URLSearchParams()
body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId)
body.append('code', code)
body.append('redirect_uri', this.redirectUrl)
const tokenRequest: AxiosRequestConfig = {
url: this.tokenUrl,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body.toString()
}
const tokenResponse = await this.bot.axios.request(tokenRequest)
const tokenData: OAuth = await tokenResponse.data
this.bot.log(this.bot.isMobile, 'LOGIN-APP', 'Successfully authorized')
return tokenData.access_token
}
private async checkAccountLocked(page: Page) {
await this.bot.utils.wait(2000)
const isLocked = await page.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
if (isLocked) {
throw this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'This account has been locked! Remove the account from "accounts.json" and restart!', 'error')
}
}
} }

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData' import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
@@ -18,12 +18,12 @@ export class Workers {
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
this.bot.log('DAILY-SET', 'All Daily Set" items have already been completed') this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
return return
} }
// Solve Activities // Solve Activities
this.bot.log('DAILY-SET', 'Started solving "Daily Set" items') this.bot.log(this.bot.isMobile, 'DAILY-SET', 'Started solving "Daily Set" items')
await this.solveActivities(page, activitiesUncompleted) await this.solveActivities(page, activitiesUncompleted)
@@ -32,33 +32,40 @@ export class Workers {
// Always return to the homepage if not already // Always return to the homepage if not already
await this.bot.browser.func.goHome(page) await this.bot.browser.func.goHome(page)
this.bot.log('DAILY-SET', 'All "Daily Set" items have been completed') this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have been completed')
} }
// Punch Card // Punch Card
async doPunchCard(page: Page, data: DashboardData) { async doPunchCard(page: Page, data: DashboardData) {
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion?.complete) ?? [] // Only return uncompleted punch cards const punchCardsUncompleted = data.punchCards?.filter(x => x.parentPromotion && !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
if (!punchCardsUncompleted.length) { if (!punchCardsUncompleted.length) {
this.bot.log('PUNCH-CARD', 'All "Punch Cards" have already been completed') this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Cards" have already been completed')
return return
} }
for (const punchCard of punchCardsUncompleted) { for (const punchCard of punchCardsUncompleted) {
// Ensure parentPromotion exists before proceeding
if (!punchCard.parentPromotion?.title) {
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Skipped punchcard "${punchCard.name}" | Reason: Parent promotion is missing!`, 'warn')
continue
}
// Get latest page for each card // Get latest page for each card
page = await this.bot.browser.utils.getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
// Solve Activities // Solve Activities
this.bot.log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`) this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
// Got to punch card index page in a new tab // Got to punch card index page in a new tab
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL }) await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
// Wait for new page to load, max 10 seconds, however try regardless in case of error // Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { }) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
await this.solveActivities(page, activitiesUncompleted, punchCard) await this.solveActivities(page, activitiesUncompleted, punchCard)
@@ -72,10 +79,10 @@ export class Workers {
await this.bot.browser.func.goHome(page) await this.bot.browser.func.goHome(page)
} }
this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`) this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
} }
this.bot.log('PUNCH-CARD', 'All "Punch Card" items have been completed') this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Card" items have been completed')
} }
// More Promotions // More Promotions
@@ -87,15 +94,15 @@ export class Workers {
morePromotions.push(data.promotionalItem as unknown as MorePromotion) morePromotions.push(data.promotionalItem as unknown as MorePromotion)
} }
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && !x.attributes.is_unlocked) ?? [] const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && x.exclusiveLockedFeatureStatus !== 'locked') ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
return return
} }
// Solve Activities // Solve Activities
this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" items') this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'Started solving "More Promotions" items')
page = await this.bot.browser.utils.getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)
@@ -106,7 +113,7 @@ export class Workers {
// Always return to the homepage if not already // Always return to the homepage if not already
await this.bot.browser.func.goHome(page) await this.bot.browser.func.goHome(page)
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed') this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
} }
// Solve all the different types of activities // Solve all the different types of activities
@@ -132,28 +139,22 @@ export class Workers {
} }
let selector = `[data-bi-id^="${activity.offerId}"]` let selector = `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
if (punchCard) { if (punchCard) {
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity) selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} else if (activity.name.toLowerCase().includes('membercenter')) { } else if (activity.name.toLowerCase().includes('membercenter') || activity.name.toLowerCase().includes('exploreonbing')) {
selector = `[data-bi-id^="${activity.name}"]` selector = `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
} }
// Click element, it will be opened in a new tab
await activityPage.click(selector)
// Select the new activity page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Wait for the new tab to fully load, ignore error. // Wait for the new tab to fully load, ignore error.
/* /*
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster, Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load. if it didn't then it gave enough time for the page to load.
*/ */
await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { }) await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(5000) await this.bot.utils.wait(2000)
switch (activity.promotionType) { switch (activity.promotionType) {
// Quiz (Poll, Quiz or ABC) // Quiz (Poll, Quiz or ABC)
@@ -163,23 +164,31 @@ export class Workers {
case 10: case 10:
// Normal poll // Normal poll
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) { if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
this.bot.log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doPoll(activityPage) await this.bot.activities.doPoll(activityPage)
} else { // ABC } else { // ABC
this.bot.log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doABC(activityPage) await this.bot.activities.doABC(activityPage)
} }
break break
// This Or That Quiz (Usually 50 points) // This Or That Quiz (Usually 50 points)
case 50: case 50:
this.bot.log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doThisOrThat(activityPage) await this.bot.activities.doThisOrThat(activityPage)
break break
// Quizzes are usually 30-40 points // Quizzes are usually 30-40 points
default: default:
this.bot.log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doQuiz(activityPage) await this.bot.activities.doQuiz(activityPage)
break break
} }
@@ -187,14 +196,24 @@ export class Workers {
// UrlReward (Visit) // UrlReward (Visit)
case 'urlreward': case 'urlreward':
this.bot.log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`) // Search on Bing are subtypes of "urlreward"
await this.bot.activities.doUrlReward(activityPage) if (activity.name.toLowerCase().includes('exploreonbing')) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "SearchOnBing" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doSearchOnBing(activityPage, activity)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doUrlReward(activityPage)
}
break break
// Misc, Usually UrlReward Type // Unsupported types
default: default:
this.bot.log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`) this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
await this.bot.activities.doUrlReward(activityPage)
break break
} }
@@ -202,7 +221,7 @@ export class Workers {
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
} catch (error) { } catch (error) {
this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -6,31 +6,31 @@ import { Workers } from '../Workers'
export class ABC extends Workers { export class ABC extends Workers {
async doABC(page: Page) { async doABC(page: Page) {
this.bot.log('ABC', 'Trying to complete poll') this.bot.log(this.bot.isMobile, 'ABC', 'Trying to complete poll')
try { try {
let $ = await this.bot.browser.func.refreshCheerio(page) let $ = await this.bot.browser.func.loadInCheerio(page)
// Don't loop more than 15 in case unable to solve, would lock otherwise // Don't loop more than 15 in case unable to solve, would lock otherwise
const maxIterations = 15 const maxIterations = 15
let i let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10000 })
const answers = $('.wk_OptionClickClass') const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id'] const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 }) await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.click(`#${answer}`) // Click answer await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(4000) await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10000 })
await page.click('div.wk_button') // Click next question button await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)
$ = await this.bot.browser.func.refreshCheerio(page) $ = await this.bot.browser.func.loadInCheerio(page)
await this.bot.utils.wait(1000) await this.bot.utils.wait(1000)
} }
@@ -38,14 +38,14 @@ export class ABC extends Workers {
await page.close() await page.close()
if (i === maxIterations) { if (i === maxIterations) {
this.bot.log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn') this.bot.log(this.bot.isMobile, 'ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
} else { } else {
this.bot.log('ABC', 'Completed the ABC successfully') this.bot.log(this.bot.isMobile, 'ABC', 'Completed the ABC successfully')
} }
} catch (error) { } catch (error) {
await page.close() await page.close()
this.bot.log('ABC', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'ABC', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -0,0 +1,48 @@
import { randomBytes } from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class DailyCheckIn extends Workers {
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'Starting Daily Check In')
try {
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
const jsonData = {
amount: 1,
country: geoLocale,
id: randomBytes(64).toString('hex'),
type: 101,
attributes: {
offerid: 'Gamification_Sapphire_DailyCheckIn'
}
}
const claimRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
},
data: JSON.stringify(jsonData)
}
const claimResponse = await this.bot.axios.request(claimRequest)
const claimedPoint = parseInt((await claimResponse.data).response?.activity?.p) ?? 0
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
} catch (error) {
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -6,12 +6,12 @@ import { Workers } from '../Workers'
export class Poll extends Workers { export class Poll extends Workers {
async doPoll(page: Page) { async doPoll(page: Page) {
this.bot.log('POLL', 'Trying to complete poll') this.bot.log(this.bot.isMobile, 'POLL', 'Trying to complete poll')
try { try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}` const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not await page.waitForSelector(buttonId, { state: 'visible', timeout: 10000 }).catch(() => { }) // We're gonna click regardless or not
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.click(buttonId) await page.click(buttonId)
@@ -19,10 +19,10 @@ export class Poll extends Workers {
await this.bot.utils.wait(4000) await this.bot.utils.wait(4000)
await page.close() await page.close()
this.bot.log('POLL', 'Completed the poll successfully') this.bot.log(this.bot.isMobile, 'POLL', 'Completed the poll successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
this.bot.log('POLL', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'POLL', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -6,7 +6,7 @@ import { Workers } from '../Workers'
export class Quiz extends Workers { export class Quiz extends Workers {
async doQuiz(page: Page) { async doQuiz(page: Page) {
this.bot.log('QUIZ', 'Trying to complete quiz') this.bot.log(this.bot.isMobile, 'QUIZ', 'Trying to complete quiz')
try { try {
// Check if the quiz has been started or not // Check if the quiz has been started or not
@@ -14,7 +14,7 @@ export class Quiz extends Workers {
if (quizNotStarted) { if (quizNotStarted) {
await page.click('#rqStartQuiz') await page.click('#rqStartQuiz')
} else { } else {
this.bot.log('QUIZ', 'Quiz has already been started, trying to finish it') this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz has already been started, trying to finish it')
} }
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
@@ -29,7 +29,7 @@ export class Quiz extends Workers {
const answers: string[] = [] const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') { if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
@@ -47,7 +47,7 @@ export class Quiz extends Workers {
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page) const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) { if (!refreshSuccess) {
await page.close() await page.close()
this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return return
} }
} }
@@ -59,7 +59,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
if (dataOption === correctOption) { if (dataOption === correctOption) {
@@ -69,7 +69,7 @@ export class Quiz extends Workers {
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page) const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) { if (!refreshSuccess) {
await page.close() await page.close()
this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return return
} }
} }
@@ -81,10 +81,11 @@ export class Quiz extends Workers {
// Done with // Done with
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.close() await page.close()
this.bot.log('QUIZ', 'Completed the quiz successfully')
this.bot.log(this.bot.isMobile, 'QUIZ', 'Completed the quiz successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
this.bot.log('QUIZ', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -0,0 +1,73 @@
import { randomBytes } from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class ReadToEarn extends Workers {
public async doReadToEarn(accessToken: string, data: DashboardData) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Starting Read to Earn')
try {
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
const userDataRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me',
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
}
}
const userDataResponse = await this.bot.axios.request(userDataRequest)
const userData = (await userDataResponse.data).response
let userBalance = userData.balance
const jsonData = {
amount: 1,
country: geoLocale,
id: '1',
type: 101,
attributes: {
offerid: 'ENUS_readarticle3_30points'
}
}
const articleCount = 10
for (let i = 0; i < articleCount; ++i) {
jsonData.id = randomBytes(64).toString('hex')
const claimRequest = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
},
data: JSON.stringify(jsonData)
}
const claimResponse = await this.bot.axios.request(claimRequest)
const newBalance = (await claimResponse.data).response.balance
if (newBalance == userBalance) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Read all available articles')
break
} else {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', `Read article ${i + 1} of ${articleCount} max | Gained ${newBalance - userBalance} Points`)
userBalance = newBalance
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
}
}
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Completed Read to Earn')
} catch (error) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -1,20 +1,27 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import axios from 'axios'
import { platform } from 'os' import { platform } from 'os'
import { Workers } from '../Workers' import { Workers } from '../Workers'
import { Counters, DashboardData } from '../../interface/DashboardData' import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleTrends } from '../../interface/GoogleDailyTrends'
import { GoogleSearch } from '../../interface/Search' import { GoogleSearch } from '../../interface/Search'
import { AxiosRequestConfig } from 'axios'
type GoogleTrendsResponse = [
string,
[
string,
...null[],
[string, ...string[]]
][]
];
export class Search extends Workers { export class Search extends Workers {
private bingHome = 'https://bing.com'
private searchPageURL = 'https://bing.com' private searchPageURL = ''
public async doSearch(page: Page, data: DashboardData) { public async doSearch(page: Page, data: DashboardData) {
this.bot.log('SEARCH-BING', 'Starting bing searches') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Starting Bing searches')
page = await this.bot.browser.utils.getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)
@@ -22,33 +29,35 @@ export class Search extends Workers {
let missingPoints = this.calculatePoints(searchCounters) let missingPoints = this.calculatePoints(searchCounters)
if (missingPoints === 0) { if (missingPoints === 0) {
this.bot.log('SEARCH-BING', `Bing searches for ${this.bot.isMobile ? 'MOBILE' : 'DESKTOP'} have already been completed`) this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Bing searches have already been completed')
return return
} }
// Generate search queries // Generate search queries
let googleSearchQueries = await this.getGoogleTrends(data.userProfile.attributes.country, missingPoints) let googleSearchQueries = await this.getGoogleTrends(this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US')
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries) googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Deduplicate the search terms // Deduplicate the search terms
googleSearchQueries = [...new Set(googleSearchQueries)] googleSearchQueries = [...new Set(googleSearchQueries)]
// Go to bing // Go to bing
await page.goto(this.searchPageURL) await page.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search await this.bot.utils.wait(2000)
await this.bot.browser.utils.tryDismissAllMessages(page)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it doesn't continue after 5 more searches with alternative queries, abort search
const queries: string[] = [] const queries: string[] = []
// Mobile search doesn't seem to like related queries? // Mobile search doesn't seem to like related queries?
googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) }) googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) })
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
// Loop over Google search queries // Loop over Google search queries
for (let i = 0; i < queries.length; i++) { for (let i = 0; i < queries.length; i++) {
const query = queries[i] as string const query = queries[i] as string
this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${this.bot.isMobile}`) this.bot.log(this.bot.isMobile, 'SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query}`)
searchCounters = await this.bingSearch(page, query) searchCounters = await this.bingSearch(page, query)
const newMissingPoints = this.calculatePoints(searchCounters) const newMissingPoints = this.calculatePoints(searchCounters)
@@ -67,14 +76,14 @@ export class Search extends Workers {
} }
// Only for mobile searches // Only for mobile searches
if (maxLoop > 3 && this.bot.isMobile) { if (maxLoop > 5 && this.bot.isMobile) {
this.bot.log('SEARCH-BING-MOBILE', 'Search didn\'t gain point for 3 iterations, likely bad User-Agent', 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
break break
} }
// If we didn't gain points for 10 iterations, assume it's stuck // If we didn't gain points for 10 iterations, assume it's stuck
if (maxLoop > 10) { if (maxLoop > 10) {
this.bot.log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
maxLoop = 0 // Reset to 0 so we can retry with related searches below maxLoop = 0 // Reset to 0 so we can retry with related searches below
break break
} }
@@ -87,7 +96,7 @@ export class Search extends Workers {
// If we still got remaining search queries, generate extra ones // If we still got remaining search queries, generate extra ones
if (missingPoints > 0) { if (missingPoints > 0) {
this.bot.log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`) this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
let i = 0 let i = 0
while (missingPoints > 0) { while (missingPoints > 0) {
@@ -98,7 +107,7 @@ export class Search extends Workers {
if (relatedTerms.length > 3) { if (relatedTerms.length > 3) {
// Search for the first 2 related terms // Search for the first 2 related terms
for (const term of relatedTerms.slice(1, 3)) { for (const term of relatedTerms.slice(1, 3)) {
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`) this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term}`)
searchCounters = await this.bingSearch(page, term) searchCounters = await this.bingSearch(page, term)
const newMissingPoints = this.calculatePoints(searchCounters) const newMissingPoints = this.calculatePoints(searchCounters)
@@ -119,7 +128,7 @@ export class Search extends Workers {
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck // Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
if (maxLoop > 5) { if (maxLoop > 5) {
this.bot.log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
return return
} }
} }
@@ -127,7 +136,7 @@ export class Search extends Workers {
} }
} }
this.bot.log('SEARCH-BING', 'Completed searches') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Completed searches')
} }
private async bingSearch(searchPage: Page, query: string) { private async bingSearch(searchPage: Page, query: string) {
@@ -136,6 +145,8 @@ export class Search extends Workers {
// Try a max of 5 times // Try a max of 5 times
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
try { try {
// This page had already been set to the Bing.com page or the previous search listing, we just need to select it
searchPage = await this.bot.browser.utils.getLatestTab(searchPage)
// Go to top of the page // Go to top of the page
await searchPage.evaluate(() => { await searchPage.evaluate(() => {
@@ -145,7 +156,7 @@ export class Search extends Workers {
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
const searchBar = '#sb_form_q' const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 }) await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await searchPage.click(searchBar) // Focus on the textarea await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
await searchPage.keyboard.down(platformControlKey) await searchPage.keyboard.down(platformControlKey)
@@ -155,84 +166,109 @@ export class Search extends Workers {
await searchPage.keyboard.type(query) await searchPage.keyboard.type(query)
await searchPage.keyboard.press('Enter') await searchPage.keyboard.press('Enter')
await this.bot.utils.wait(3000)
// Bing.com in Chrome opens a new tab when searching
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
this.searchPageURL = new URL(resultPage.url()).href // Set the results page
await this.bot.browser.utils.reloadBadPage(resultPage)
if (this.bot.config.searchSettings.scrollRandomResults) { if (this.bot.config.searchSettings.scrollRandomResults) {
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await this.randomScroll(searchPage) await this.randomScroll(resultPage)
} }
if (this.bot.config.searchSettings.clickRandomResults) { if (this.bot.config.searchSettings.clickRandomResults) {
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await this.clickRandomLink(searchPage) await this.clickRandomLink(resultPage)
} }
// Delay between searches // Delay between searches
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max))) await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
return await this.bot.browser.func.getSearchPoints() return await this.bot.browser.func.getSearchPoints()
} catch (error) { } catch (error) {
if (i === 5) { if (i === 5) {
this.bot.log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
break break
} }
this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
// Reset the tabs // Reset the tabs
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage) const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
await this.closeTabs(lastTab, this.searchPageURL) await this.closeTabs(lastTab)
await this.bot.utils.wait(4000) await this.bot.utils.wait(4000)
} }
} }
this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error') this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
return await this.bot.browser.func.getSearchPoints() return await this.bot.browser.func.getSearchPoints()
} }
private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> { private async getGoogleTrends(geoLocale: string = 'US'): Promise<GoogleSearch[]> {
const queryTerms: GoogleSearch[] = [] const queryTerms: GoogleSearch[] = []
let i = 0 this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US' try {
const request: AxiosRequestConfig = {
this.bot.log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`) url: 'https://trends.google.com/_/TrendsUi/data/batchexecute',
method: 'POST',
while (queryCount > queryTerms.length) { headers: {
i += 1 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
const date = new Date() },
date.setDate(date.getDate() - i) data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale.toUpperCase()}\\", 0, null, 48]"]]]`
const formattedDate = this.formatDate(date)
try {
const request = {
url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
const response = await axios(request)
const data: GoogleTrends = JSON.parse((await response.data).slice(5))
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
queryTerms.push({
topic: topic.title.query.toLowerCase(),
related: topic.relatedQueries.map(x => x.query.toLowerCase())
})
}
} catch (error) {
this.bot.log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
const rawText = response.data
const trendsData = this.extractJsonFromResponse(rawText)
if (!trendsData) {
throw this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
}
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
if (mappedTrendsData.length < 90) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Insufficient search queries, falling back to US', 'warn')
return this.getGoogleTrends()
}
for (const [topic, relatedQueries] of mappedTrendsData) {
queryTerms.push({
topic: topic as string,
related: relatedQueries as string[]
})
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
} }
return queryTerms return queryTerms
} }
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
const lines = text.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch {
continue
}
}
}
return null
}
private async getRelatedTerms(term: string): Promise<string[]> { private async getRelatedTerms(term: string): Promise<string[]> {
try { try {
const request = { const request = {
@@ -243,55 +279,51 @@ export class Search extends Workers {
} }
} }
const response = await axios(request) const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyBingTerms)
return response.data[1] as string[] return response.data[1] as string[]
} catch (error) { } catch (error) {
this.bot.log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'SEARCH-BING-RELATED', 'An error occurred:' + error, 'error')
} }
return [] return []
} }
private formatDate(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}${month}${day}`
}
private async randomScroll(page: Page) { private async randomScroll(page: Page) {
try { try {
const scrollAmount = this.bot.utils.randomNumber(5, 5000) const viewportHeight = await page.evaluate(() => window.innerHeight)
const totalHeight = await page.evaluate(() => document.body.scrollHeight)
const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
await page.evaluate((scrollAmount) => { await page.evaluate((scrollPos) => {
window.scrollBy(0, scrollAmount) window.scrollTo(0, scrollPos)
}, scrollAmount) }, randomScrollPosition)
} catch (error) { } catch (error) {
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
} }
} }
private async clickRandomLink(page: Page) { private async clickRandomLink(page: Page) {
try { try {
const searchListingURL = new URL(page.url()) // Get searchPage info before clicking await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not // Only used if the browser is not the edge browser (continue on Edge popup)
await this.closeContinuePopup(page)
// Will get current tab if no new one is created // Stay for 10 seconds for page to load and "visit"
await this.bot.utils.wait(10000)
// Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
let lastTab = await this.bot.browser.utils.getLatestTab(page) let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Let website load, if it doesn't load within 5 sec. exit regardless let lastTabURL = new URL(lastTab.url()) // Get new tab info, this is the website we're visiting
await this.bot.utils.wait(5000)
let lastTabURL = new URL(lastTab.url()) // Get new tab info
// Check if the URL is different from the original one, don't loop more than 5 times. // Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0 let i = 0
while (lastTabURL.href !== searchListingURL.href && i < 5) { while (lastTabURL.href !== this.searchPageURL && i < 5) {
await this.closeTabs(lastTab, searchListingURL.href) await this.closeTabs(lastTab)
// End of loop, refresh lastPage // End of loop, refresh lastPage
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
@@ -300,29 +332,44 @@ export class Search extends Workers {
} }
} catch (error) { } catch (error) {
this.bot.log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
} }
} }
private async closeTabs(lastTab: Page, url: string) { private async closeTabs(lastTab: Page) {
const browser = lastTab.context() const browser = lastTab.context()
const tabs = browser.pages() const tabs = browser.pages()
// If more than 3 tabs are open, close the last tab try {
if (tabs.length > 2) { if (tabs.length > 2) {
await lastTab.close() // If more than 2 tabs are open, close the last tab
// If only 1 tab is open, open a new one to search in await lastTab.close()
} else if (tabs.length === 1) { this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', `More than 2 were open, closed the last tab: "${new URL(lastTab.url()).host}"`)
const newPage = await browser.newPage()
await newPage.goto(url)
// Else go back one page } else if (tabs.length === 1) {
} else { // If only 1 tab is open, open a new one to search in
await lastTab.goBack()
const newPage = await browser.newPage()
await this.bot.utils.wait(1000)
await newPage.goto(this.bingHome)
await this.bot.utils.wait(3000)
this.searchPageURL = newPage.url()
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'There was only 1 tab open, crated a new one')
} else {
// Else reset the last tab back to the search listing or Bing.com
lastTab = await this.bot.browser.utils.getLatestTab(lastTab)
await lastTab.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'An error occurred:' + error, 'error')
} }
}
}
private calculatePoints(counters: Counters) { private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches const mobileData = counters.mobileSearch?.[0] // Mobile searches
@@ -336,4 +383,18 @@ export class Search extends Workers {
return missingPoints return missingPoints
} }
private async closeContinuePopup(page: Page) {
try {
await page.waitForSelector('#sacs_close', { timeout: 1000 })
const continueButton = await page.$('#sacs_close')
if (continueButton) {
await continueButton.click()
}
} catch (error) {
// Continue if element is not found or other error occurs
}
}
} }

View File

@@ -0,0 +1,75 @@
import { Page } from 'rebrowser-playwright'
import * as fs from 'fs'
import path from 'path'
import { Workers } from '../Workers'
import { MorePromotion, PromotionalItem } from '../../interface/DashboardData'
export class SearchOnBing extends Workers {
async doSearchOnBing(page: Page, activity: MorePromotion | PromotionalItem) {
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Trying to complete SearchOnBing')
try {
await this.bot.utils.wait(5000)
await this.bot.browser.utils.tryDismissAllMessages(page)
const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q'
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await page.click(searchBar)
await this.bot.utils.wait(500)
await page.keyboard.type(query)
await page.keyboard.press('Enter')
await this.bot.utils.wait(3000)
await page.close()
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Completed the SearchOnBing successfully')
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'An error occurred:' + error, 'error')
}
}
private async getSearchQuery(title: string): Promise<string> {
interface Queries {
title: string;
queries: string[]
}
let queries: Queries[] = []
try {
if (this.bot.config.searchOnBingLocalQueries) {
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8')
queries = JSON.parse(data)
} else {
// Fetch from the repo directly so the user doesn't need to redownload the script for the new activities
const response = await this.bot.axios.request({
method: 'GET',
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/main/src/functions/queries.json'
})
queries = response.data
}
const answers = queries.find(x => this.normalizeString(x.title) === this.normalizeString(title))
const answer = answers ? this.bot.utils.shuffleArray(answers?.queries)[0] as string : title
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', `Fetched answer: ${answer} | question: ${title}`)
return answer
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'An error occurred:' + error, 'error')
return title
}
}
private normalizeString(string: string): string {
return string.normalize('NFD').trim().toLowerCase().replace(/[^\x20-\x7E]/g, '').replace(/[?!]/g, '')
}
}

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -6,7 +6,7 @@ import { Workers } from '../Workers'
export class ThisOrThat extends Workers { export class ThisOrThat extends Workers {
async doThisOrThat(page: Page) { async doThisOrThat(page: Page) {
this.bot.log('THIS-OR-THAT', 'Trying to complete ThisOrThat') this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Trying to complete ThisOrThat')
try { try {
@@ -15,7 +15,7 @@ export class ThisOrThat extends Workers {
if (quizNotStarted) { if (quizNotStarted) {
await page.click('#rqStartQuiz') await page.click('#rqStartQuiz')
} else { } else {
this.bot.log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it') this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
} }
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
@@ -32,15 +32,15 @@ export class ThisOrThat extends Workers {
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page) const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) { if (!refreshSuccess) {
await page.close() await page.close()
this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return return
} }
} }
this.bot.log('THIS-OR-THAT', 'Completed the ThisOrThat successfully') this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Completed the ThisOrThat successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
this.bot.log('THIS-OR-THAT', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -1,4 +1,4 @@
import { Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -6,17 +6,17 @@ import { Workers } from '../Workers'
export class UrlReward extends Workers { export class UrlReward extends Workers {
async doUrlReward(page: Page) { async doUrlReward(page: Page) {
this.bot.log('URL-REWARD', 'Trying to complete UrlReward') this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Trying to complete UrlReward')
try { try {
this.bot.utils.wait(2000) this.bot.utils.wait(2000)
await page.close() await page.close()
this.bot.log('URL-REWARD', 'Completed the UrlReward successfully') this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Completed the UrlReward successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
this.bot.log('URL-REWARD', 'An error occurred:' + error, 'error') this.bot.log(this.bot.isMobile, 'URL-REWARD', 'An error occurred:' + error, 'error')
} }
} }

289
src/functions/queries.json Normal file
View File

@@ -0,0 +1,289 @@
[
{
"title": "Houses near you",
"queries": [
"Houses near me"
]
},
{
"title": "Feeling symptoms?",
"queries": [
"Rash on forearm",
"Stuffy nose",
"Tickling cough"
]
},
{
"title": "Get your shopping done faster",
"queries": [
"Buy PS5",
"Buy Xbox",
"Chair deals"
]
},
{
"title": "Translate anything",
"queries": [
"Translate welcome home to Korean",
"Translate welcome home to Japanese",
"Translate goodbye to Japanese"
]
},
{
"title": "Search the lyrics of a song",
"queries": [
"Debarge rhythm of the night lyrics"
]
},
{
"title": "Let's watch that movie again!",
"queries": [
"Alien movie",
"Aliens movie",
"Alien 3 movie",
"Predator movie"
]
},
{
"title": "Plan a quick getaway",
"queries": [
"Flights Amsterdam to Tokyo",
"Flights New York to Tokyo"
]
},
{
"title": "Discover open job roles",
"queries": [
"jobs at Microsoft",
"Microsoft Job Openings",
"Jobs near me",
"jobs at Boeing worked"
]
},
{
"title": "You can track your package",
"queries": [
"USPS tracking"
]
},
{
"title": "Find somewhere new to explore",
"queries": [
"Directions to Berlin",
"Directions to Tokyo",
"Directions to New York"
]
},
{
"title": "Too tired to cook tonight?",
"queries": [
"KFC near me",
"Burger King near me",
"McDonalds near me"
]
},
{
"title": "Quickly convert your money",
"queries": [
"convert 250 USD to yen",
"convert 500 USD to yen"
]
},
{
"title": "Learn to cook a new recipe",
"queries": [
"How to cook ratatouille",
"How to cook lasagna"
]
},
{
"title": "Find places to stay!",
"queries": [
"Hotels Berlin Germany",
"Hotels Amsterdam Netherlands"
]
},
{
"title": "How's the economy?",
"queries": [
"sp 500"
]
},
{
"title": "Who won?",
"queries": [
"braves score"
]
},
{
"title": "Gaming time",
"queries": [
"Overwatch video game",
"Call of duty video game"
]
},
{
"title": "Expand your vocabulary",
"queries": [
"definition definition"
]
},
{
"title": "What time is it?",
"queries": [
"Japan time",
"New York time"
]
},
{
"title": "Maisons près de chez vous",
"queries": [
"Maisons près de chez moi"
]
},
{
"title": "Vous ressentez des symptômes ?",
"queries": [
"Éruption cutanée sur l'avant-bras",
"Nez bouché",
"Toux chatouilleuse"
]
},
{
"title": "Faites vos achats plus vite",
"queries": [
"Acheter une PS5",
"Acheter une Xbox",
"Offres sur les chaises"
]
},
{
"title": "Traduisez tout !",
"queries": [
"Traduction bienvenue à la maison en coréen",
"Traduction bienvenue à la maison en japonais",
"Traduction au revoir en japonais"
]
},
{
"title": "Rechercher paroles de chanson",
"queries": [
"Paroles de Debarge rhythm of the night"
]
},
{
"title": "Et si nous regardions ce film une nouvelle fois?",
"queries": [
"Alien film",
"Film Aliens",
"Film Alien 3",
"Film Predator"
]
},
{
"title": "Planifiez une petite escapade",
"queries": [
"Vols Amsterdam-Tokyo",
"Vols New York-Tokyo"
]
},
{
"title": "Consulter postes à pourvoir",
"queries": [
"emplois chez Microsoft",
"Offres d'emploi Microsoft",
"Emplois près de chez moi",
"emplois chez Boeing"
]
},
{
"title": "Vous pouvez suivre votre colis",
"queries": [
"Suivi Chronopost"
]
},
{
"title": "Trouver un endroit à découvrir",
"queries": [
"Itinéraire vers Berlin",
"Itinéraire vers Tokyo",
"Itinéraire vers New York"
]
},
{
"title": "Trop fatigué pour cuisiner ce soir ?",
"queries": [
"KFC près de chez moi",
"Burger King près de chez moi",
"McDonalds près de chez moi"
]
},
{
"title": "Convertissez rapidement votre argent",
"queries": [
"convertir 250 EUR en yen",
"convertir 500 EUR en yen"
]
},
{
"title": "Apprenez à cuisiner une nouvelle recette",
"queries": [
"Comment faire cuire la ratatouille",
"Comment faire cuire les lasagnes"
]
},
{
"title": "Trouvez des emplacements pour rester!",
"queries": [
"Hôtels Berlin Allemagne",
"Hôtels Amsterdam Pays-Bas"
]
},
{
"title": "Comment se porte l'économie ?",
"queries": [
"CAC 40"
]
},
{
"title": "Qui a gagné ?",
"queries": [
"score du Paris Saint-Germain"
]
},
{
"title": "Temps de jeu",
"queries": [
"Jeu vidéo Overwatch",
"Jeu vidéo Call of Duty"
]
},
{
"title": "Enrichissez votre vocabulaire",
"queries": [
"definition definition"
]
},
{
"title": "Quelle heure est-il ?",
"queries": [
"Heure du Japon",
"Heure de New York"
]
},
{
"title": "Vérifier la météo",
"queries": [
"Météo de Paris",
"Météo de la France"
]
},
{
"title": "Tenez-vous informé des sujets d'actualité",
"queries": [
"Augmentation Impots",
"Mort célébrité"
]
}
]

View File

@@ -1,5 +1,5 @@
import cluster from 'cluster' import cluster from 'cluster'
import { BrowserContext, Page } from 'playwright' import { Page } from 'rebrowser-playwright'
import Browser from './browser/Browser' import Browser from './browser/Browser'
import BrowserFunc from './browser/BrowserFunc' import BrowserFunc from './browser/BrowserFunc'
@@ -14,6 +14,8 @@ import { Workers } from './functions/Workers'
import Activities from './functions/Activities' import Activities from './functions/Activities'
import { Account } from './interface/Account' import { Account } from './interface/Account'
import Axios from './util/Axios'
// Main bot class // Main bot class
export class MicrosoftRewardsBot { export class MicrosoftRewardsBot {
@@ -25,17 +27,25 @@ export class MicrosoftRewardsBot {
func: BrowserFunc, func: BrowserFunc,
utils: BrowserUtil utils: BrowserUtil
} }
public isMobile: boolean = false public isMobile: boolean
public homePage!: Page public homePage!: Page
private collectedPoints: number = 0 private pointsCanCollect: number = 0
private pointsInitial: number = 0
private activeWorkers: number private activeWorkers: number
private mobileRetryAttempts: number
private browserFactory: Browser = new Browser(this) private browserFactory: Browser = new Browser(this)
private accounts: Account[] private accounts: Account[]
private workers: Workers private workers: Workers
private login = new Login(this) private login = new Login(this)
private accessToken: string = ''
constructor() { //@ts-expect-error Will be initialized later
public axios: Axios
constructor(isMobile: boolean) {
this.isMobile = isMobile
this.log = log this.log = log
this.accounts = [] this.accounts = []
@@ -47,6 +57,7 @@ export class MicrosoftRewardsBot {
} }
this.config = loadConfig() this.config = loadConfig()
this.activeWorkers = this.config.clusters this.activeWorkers = this.config.clusters
this.mobileRetryAttempts = 0
} }
async initialize() { async initialize() {
@@ -54,7 +65,7 @@ export class MicrosoftRewardsBot {
} }
async run() { async run() {
log('MAIN', `Bot started with ${this.config.clusters} clusters`) log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`)
// Only cluster when there's more than 1 cluster demanded // Only cluster when there's more than 1 cluster demanded
if (this.config.clusters > 1) { if (this.config.clusters > 1) {
@@ -64,12 +75,12 @@ export class MicrosoftRewardsBot {
this.runWorker() this.runWorker()
} }
} else { } else {
this.runTasks(this.accounts) await this.runTasks(this.accounts)
} }
} }
private runMaster() { private runMaster() {
log('MAIN-PRIMARY', 'Primary process started') log('main', 'MAIN-PRIMARY', 'Primary process started')
const accountChunks = this.utils.chunkArray(this.accounts, this.config.clusters) const accountChunks = this.utils.chunkArray(this.accounts, this.config.clusters)
@@ -82,18 +93,18 @@ export class MicrosoftRewardsBot {
cluster.on('exit', (worker, code) => { cluster.on('exit', (worker, code) => {
this.activeWorkers -= 1 this.activeWorkers -= 1
log('MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn') log('main', 'MAIN-WORKER', `Worker ${worker.process.pid} destroyed | Code: ${code} | Active workers: ${this.activeWorkers}`, 'warn')
// Check if all workers have exited // Check if all workers have exited
if (this.activeWorkers === 0) { if (this.activeWorkers === 0) {
log('MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn') log('main', 'MAIN-WORKER', 'All workers destroyed. Exiting main process!', 'warn')
process.exit(0) process.exit(0)
} }
}) })
} }
private runWorker() { private runWorker() {
log('MAIN-WORKER', `Worker ${process.pid} spawned`) log('main', 'MAIN-WORKER', `Worker ${process.pid} spawned`)
// Receive the chunk of accounts from the master // Receive the chunk of accounts from the master
process.on('message', async ({ chunk }) => { process.on('message', async ({ chunk }) => {
await this.runTasks(chunk) await this.runTasks(chunk)
@@ -102,53 +113,68 @@ export class MicrosoftRewardsBot {
private async runTasks(accounts: Account[]) { private async runTasks(accounts: Account[]) {
for (const account of accounts) { for (const account of accounts) {
log('MAIN-WORKER', `Started tasks for account ${account.email}`) log('main', 'MAIN-WORKER', `Started tasks for account ${account.email}`)
// Desktop Searches, DailySet and More Promotions this.axios = new Axios(account.proxy)
await this.Desktop(account) if (this.config.parallel) {
await Promise.all([
this.Desktop(account),
(() => {
const mobileInstance = new MicrosoftRewardsBot(true)
mobileInstance.axios = this.axios
// If runOnZeroPoints is false and 0 points to earn, stop and try the next account return mobileInstance.Mobile(account)
if (!this.config.runOnZeroPoints && this.collectedPoints === 0) { })()
continue ])
} else {
this.isMobile = false
await this.Desktop(account)
this.isMobile = true
await this.Mobile(account)
} }
// Mobile Searches log('main', 'MAIN-WORKER', `Completed tasks for account ${account.email}`, 'log', 'green')
await this.Mobile(account)
log('MAIN-WORKER', `Completed tasks for account ${account.email}`)
} }
log('MAIN-PRIMARY', 'Completed tasks for ALL accounts') log(this.isMobile, 'MAIN-PRIMARY', 'Completed tasks for ALL accounts', 'log', 'green')
log('MAIN-PRIMARY', 'All workers destroyed!') process.exit()
process.exit(0)
} }
// Desktop // Desktop
async Desktop(account: Account) { async Desktop(account: Account) {
this.isMobile = false
const browser = await this.browserFactory.createBrowser(account.proxy, account.email) const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
this.homePage = await browser.newPage() this.homePage = await browser.newPage()
log('MAIN', 'Starting DESKTOP browser') log(this.isMobile, 'MAIN', 'Starting browser')
// Login into MS Rewards, then go to rewards homepage // Login into MS Rewards, then go to rewards homepage
await this.login.login(this.homePage, account.email, account.password) await this.login.login(this.homePage, account.email, account.password)
await this.browser.func.goHome(this.homePage) await this.browser.func.goHome(this.homePage)
const data = await this.browser.func.getDashboardData() const data = await this.browser.func.getDashboardData()
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
const earnablePoints = await this.browser.func.getEarnablePoints() this.pointsInitial = data.userStatus.availablePoints
this.collectedPoints = earnablePoints
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`) log(this.isMobile, 'MAIN-POINTS', `Current point count: ${this.pointsInitial}`)
const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
// Tally all the desktop points
this.pointsCanCollect = browserEnarablePoints.dailySetPoints +
browserEnarablePoints.desktopSearchPoints
+ browserEnarablePoints.morePromotionsPoints
log(this.isMobile, 'MAIN-POINTS', `You can earn ${this.pointsCanCollect} points today`)
// If runOnZeroPoints is false and 0 points to earn, don't continue // If runOnZeroPoints is false and 0 points to earn, don't continue
if (!this.config.runOnZeroPoints && this.collectedPoints === 0) { if (!this.config.runOnZeroPoints && this.pointsCanCollect === 0) {
log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!') log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow')
// Close desktop browser // Close desktop browser
return await this.closeBrowser(browser, account.email) await this.browser.func.closeBrowser(browser, account.email)
return
} }
// Open a new tab to where the tasks are going to be completed // Open a new tab to where the tasks are going to be completed
@@ -181,85 +207,113 @@ export class MicrosoftRewardsBot {
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile) await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
// Close desktop browser // Close desktop browser
await this.closeBrowser(browser, account.email) await this.browser.func.closeBrowser(browser, account.email)
return return
} }
// Mobile // Mobile
async Mobile(account: Account) { async Mobile(account: Account) {
this.isMobile = true
const browser = await this.browserFactory.createBrowser(account.proxy, account.email) const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
this.homePage = await browser.newPage() this.homePage = await browser.newPage()
log('MAIN', 'Starting MOBILE browser') log(this.isMobile, 'MAIN', 'Starting browser')
// Login into MS Rewards, then go to rewards homepage // Login into MS Rewards, then go to rewards homepage
await this.login.login(this.homePage, account.email, account.password) await this.login.login(this.homePage, account.email, account.password)
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
await this.browser.func.goHome(this.homePage) await this.browser.func.goHome(this.homePage)
const data = await this.browser.func.getDashboardData() const data = await this.browser.func.getDashboardData()
// If no mobile searches data found, stop (Does not exist on new accounts) const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
if (!data.userStatus.counters.mobileSearch) { const appEarnablePoints = await this.browser.func.getAppEarnablePoints(this.accessToken)
log('MAIN', 'No mobile searches found, stopping!')
this.pointsCanCollect = browserEnarablePoints.mobileSearchPoints + appEarnablePoints.totalEarnablePoints
log(this.isMobile, 'MAIN-POINTS', `You can earn ${this.pointsCanCollect} points today (Browser: ${browserEnarablePoints.mobileSearchPoints} points, App: ${appEarnablePoints.totalEarnablePoints} points)`)
// If runOnZeroPoints is false and 0 points to earn, don't continue
if (!this.config.runOnZeroPoints && this.pointsCanCollect === 0) {
log(this.isMobile, 'MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!', 'log', 'yellow')
// Close mobile browser // Close mobile browser
return await this.closeBrowser(browser, account.email) await this.browser.func.closeBrowser(browser, account.email)
return
} }
// Open a new tab to where the tasks are going to be completed // Do daily check in
const workerPage = await browser.newPage() if (this.config.workers.doDailyCheckIn) {
await this.activities.doDailyCheckIn(this.accessToken, data)
}
// Go to homepage on worker page // Do read to earn
await this.browser.func.goHome(workerPage) if (this.config.workers.doReadToEarn) {
await this.activities.doReadToEarn(this.accessToken, data)
}
// Do mobile searches // Do mobile searches
if (this.config.workers.doMobileSearch) { if (this.config.workers.doMobileSearch) {
await this.activities.doSearch(workerPage, data) // If no mobile searches data found, stop (Does not always exist on new accounts)
if (data.userStatus.counters.mobileSearch) {
// Open a new tab to where the tasks are going to be completed
const workerPage = await browser.newPage()
// Fetch current search points // Go to homepage on worker page
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] await this.browser.func.goHome(workerPage)
// If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid await this.activities.doSearch(workerPage, data)
// Retry until all points are gathered when (retryMobileSearch is enabled)
if (this.config.searchSettings.retryMobileSearch && mobileSearchPoints && ((mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0)) {
log('MAIN', 'Unable to complete mobile searches, bad User-Agent? Retrying...')
// Close mobile browser // Fetch current search points
await this.closeBrowser(browser, account.email) const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
// Retry if (mobileSearchPoints && (mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0) {
await this.Mobile(account) // Increment retry count
this.mobileRetryAttempts++
}
// Exit if retries are exhausted
if (this.mobileRetryAttempts > this.config.searchSettings.retryMobileSearchAmount) {
log(this.isMobile, 'MAIN', `Max retry limit of ${this.config.searchSettings.retryMobileSearchAmount} reached. Exiting retry loop`, 'warn')
} else if (this.mobileRetryAttempts !== 0) {
log(this.isMobile, 'MAIN', `Attempt ${this.mobileRetryAttempts}/${this.config.searchSettings.retryMobileSearchAmount}: Unable to complete mobile searches, bad User-Agent? Increase search delay? Retrying...`, 'log', 'yellow')
// Close mobile browser
await this.browser.func.closeBrowser(browser, account.email)
// Create a new browser and try
await this.Mobile(account)
return
}
} else {
log(this.isMobile, 'MAIN', 'Unable to fetch search points, your account is most likely too "new" for this! Try again later!', 'warn')
} }
} }
// Fetch new points const afterPointAmount = await this.browser.func.getCurrentPoints()
const earnablePoints = await this.browser.func.getEarnablePoints()
// If the new earnable is 0, means we got all the points, else retract log(this.isMobile, 'MAIN-POINTS', `The script collected ${afterPointAmount - this.pointsInitial} points today`)
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
// Close mobile browser // Close mobile browser
await this.closeBrowser(browser, account.email) await this.browser.func.closeBrowser(browser, account.email)
return return
} }
private async closeBrowser(browser: BrowserContext, email: string) {
// Save cookies
await saveSessionData(this.config.sessionPath, browser, email, this.isMobile)
// Close browser
await browser.close()
}
} }
async function main() {
const rewardsBot = new MicrosoftRewardsBot(false)
const bot = new MicrosoftRewardsBot() try {
await rewardsBot.initialize()
await rewardsBot.run()
} catch (error) {
log(false, 'MAIN-ERROR', `Error running desktop bot: ${error}`, 'error')
}
}
// Initialize accounts first and then start the bot // Start the bots
bot.initialize().then(() => { main().catch(error => {
bot.run() log('main', 'MAIN-ERROR', `Error running bots: ${error}`, 'error')
process.exit(1)
}) })

View File

@@ -5,6 +5,7 @@ export interface Account {
} }
export interface AccountProxy { export interface AccountProxy {
proxyAxios: boolean;
url: string; url: string;
port: number; port: number;
password: string; password: string;

View File

@@ -0,0 +1,226 @@
export interface AppUserData {
response: Response;
correlationId: string;
code: number;
}
export interface Response {
profile: Profile;
balance: number;
counters: null;
promotions: Promotion[];
catalog: null;
goal_item: GoalItem;
activities: null;
cashback: null;
orders: Order[];
rebateProfile: null;
rebatePayouts: null;
giveProfile: GiveProfile;
autoRedeemProfile: null;
autoRedeemItem: null;
thirdPartyProfile: null;
notifications: null;
waitlist: null;
autoOpenFlyout: null;
coupons: null;
recommendedAffordableCatalog: null;
}
export interface GiveProfile {
give_user: string;
give_organization: { [key: string]: GiveOrganization | null };
first_give_optin: string;
last_give_optout: string;
give_lifetime_balance: string;
give_lifetime_donation_balance: string;
give_balance: string;
form: null;
}
export interface GiveOrganization {
give_organization_donation_points: number;
give_organization_donation_point_to_currency_ratio: number;
give_organization_donation_currency: number;
}
export interface GoalItem {
name: string;
provider: string;
price: number;
attributes: GoalItemAttributes;
config: GoalItemConfig;
}
export interface GoalItemAttributes {
category: string;
CategoryDescription: string;
'desc.group_text': string;
'desc.legal_text'?: string;
'desc.sc_description': string;
'desc.sc_title': string;
display_order: string;
ExtraLargeImage: string;
group: string;
group_image: string;
group_sc_image: string;
group_title: string;
hidden?: string;
large_image: string;
large_sc_image: string;
medium_image: string;
MobileImage: string;
original_price: string;
Remarks?: string;
ShortText?: string;
showcase?: string;
small_image: string;
title: string;
cimsid: string;
user_defined_goal?: string;
disable_bot_redemptions?: string;
'desc.large_text'?: string;
english_title?: string;
etid?: string;
sku?: string;
coupon_discount?: string;
}
export interface GoalItemConfig {
amount: string;
currencyCode: string;
isHidden: string;
PointToCurrencyConversionRatio: string;
}
export interface Order {
id: string;
t: Date;
sku: string;
item_snapshot: ItemSnapshot;
p: number;
s: S;
a: A;
child_redemption: null;
third_party_partner: null;
log: Log[];
}
export interface A {
form?: string;
OrderId: string;
CorrelationId: string;
Channel: string;
Language: string;
Country: string;
EvaluationId: string;
provider?: string;
referenceOrderID?: string;
externalRefID?: string;
denomination?: string;
rewardName?: string;
sendEmail?: string;
status?: string;
createdAt?: Date;
bal_before_deduct?: string;
bal_after_deduct?: string;
}
export interface ItemSnapshot {
name: string;
provider: string;
price: number;
attributes: GoalItemAttributes;
config: ItemSnapshotConfig;
}
export interface ItemSnapshotConfig {
amount: string;
countryCode: string;
currencyCode: string;
sku: string;
}
export interface Log {
time: Date;
from: From;
to: S;
reason: string;
}
export enum From {
Created = 'Created',
RiskApproved = 'RiskApproved',
RiskReview = 'RiskReview'
}
export enum S {
Cancelled = 'Cancelled',
RiskApproved = 'RiskApproved',
RiskReview = 'RiskReview',
Shipped = 'Shipped'
}
export interface Profile {
ruid: string;
attributes: ProfileAttributes;
offline_attributes: OfflineAttributes;
}
export interface ProfileAttributes {
publisher: string;
publisher_upd: Date;
creative: string;
creative_upd: Date;
program: string;
program_upd: Date;
country: string;
country_upd: Date;
referrerhash: string;
referrerhash_upd: Date;
optout_upd: Date;
language: string;
language_upd: Date;
target: string;
target_upd: Date;
created: Date;
created_upd: Date;
epuid: string;
epuid_upd: Date;
goal: string;
goal_upd: Date;
waitlistattributes: string;
waitlistattributes_upd: Date;
serpbotscore_upd: Date;
iscashbackeligible: string;
cbedc: string;
rlscpct_upd: Date;
give_user: string;
rebcpc_upd: Date;
SerpBotScore_upd: Date;
AdsBotScore_upd: Date;
dbs_upd: Date;
rbs: string;
rbs_upd: Date;
iris_segmentation: string;
iris_segmentation_upd: Date;
}
export interface OfflineAttributes {
}
export interface Promotion {
name: string;
priority: number;
attributes: { [key: string]: string };
tags: Tag[];
}
export enum Tag {
AllowTrialUser = 'allow_trial_user',
ExcludeGivePcparent = 'exclude_give_pcparent',
ExcludeGlobalConfig = 'exclude_global_config',
ExcludeHidden = 'exclude_hidden',
LOCString = 'locString',
NonGlobalConfig = 'non_global_config'
}

View File

@@ -2,37 +2,54 @@ export interface Config {
baseURL: string; baseURL: string;
sessionPath: string; sessionPath: string;
headless: boolean; headless: boolean;
parallel: boolean;
runOnZeroPoints: boolean; runOnZeroPoints: boolean;
clusters: number; clusters: number;
workers: Workers; saveFingerprint: ConfigSaveFingerprint;
globalTimeout: number; workers: ConfigWorkers;
searchSettings: SearchSettings; searchOnBingLocalQueries: boolean;
webhook: Webhook; globalTimeout: number | string;
saveFingerprint: boolean; searchSettings: ConfigSearchSettings;
logExcludeFunc: string[];
webhookLogExcludeFunc: string[];
proxy: ConfigProxy;
webhook: ConfigWebhook;
} }
export interface SearchSettings { export interface ConfigSaveFingerprint {
mobile: boolean;
desktop: boolean;
}
export interface ConfigSearchSettings {
useGeoLocaleQueries: boolean; useGeoLocaleQueries: boolean;
scrollRandomResults: boolean; scrollRandomResults: boolean;
clickRandomResults: boolean; clickRandomResults: boolean;
searchDelay: SearchDelay; searchDelay: ConfigSearchDelay;
retryMobileSearch: boolean; retryMobileSearchAmount: number;
} }
export interface SearchDelay { export interface ConfigSearchDelay {
min: number; min: number | string;
max: number; max: number | string;
} }
export interface Webhook { export interface ConfigWebhook {
enabled: boolean; enabled: boolean;
url: string; url: string;
} }
export interface Workers { export interface ConfigProxy {
proxyGoogleTrends: boolean;
proxyBingTerms: boolean;
}
export interface ConfigWorkers {
doDailySet: boolean; doDailySet: boolean;
doMorePromotions: boolean; doMorePromotions: boolean;
doPunchCards: boolean; doPunchCards: boolean;
doDesktopSearch: boolean; doDesktopSearch: boolean;
doMobileSearch: boolean; doMobileSearch: boolean;
doDailyCheckIn: boolean;
doReadToEarn: boolean;
} }

View File

@@ -351,11 +351,13 @@ export interface MorePromotion {
legalText: string; legalText: string;
legalLinkText: string; legalLinkText: string;
deviceType: string; deviceType: string;
exclusiveLockedFeatureType: string;
exclusiveLockedFeatureStatus: string;
} }
export interface PunchCard { export interface PunchCard {
name: string; name: string;
parentPromotion: PromotionalItem; parentPromotion?: PromotionalItem;
childPromotions: PromotionalItem[]; childPromotions: PromotionalItem[];
} }

9
src/interface/OAuth.ts Normal file
View File

@@ -0,0 +1,9 @@
export interface OAuth {
access_token: string;
refresh_token: string;
scope: string;
expires_in: number;
ext_expires_in: number;
foci: string;
token_type: string;
}

7
src/interface/Points.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface EarnablePoints {
desktopSearchPoints: number
mobileSearchPoints: number
dailySetPoints: number
morePromotionsPoints: number
totalEarnablePoints: number
}

View File

@@ -1,41 +0,0 @@
const fs = require('fs');
const path = require('path');
const configPath = path.join(__dirname, '../dist/config.json');
// Read the existing config file
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
// Update the config with environment variables if they are set
config.baseURL = process.env.BASE_URL || config.baseURL;
config.sessionPath = process.env.SESSION_PATH || config.sessionPath;
config.headless = process.env.HEADLESS ? process.env.HEADLESS === 'true' : config.headless;
config.runOnZeroPoints = process.env.RUN_ON_ZERO_POINTS ? process.env.RUN_ON_ZERO_POINTS === 'true' : config.runOnZeroPoints;
config.clusters = process.env.CLUSTERS ? parseInt(process.env.CLUSTERS, 10) : config.clusters;
config.saveFingerprint = process.env.SAVE_FINGERPRINT ? process.env.SAVE_FINGERPRINT === 'true' : config.saveFingerprint;
config.globalTimeout = process.env.GLOBAL_TIMEOUT ? parseInt(process.env.GLOBAL_TIMEOUT, 10) : config.globalTimeout;
config.workers.doDailySet = process.env.DO_DAILY_SET ? process.env.DO_DAILY_SET === 'true' : config.workers.doDailySet;
config.workers.doMorePromotions = process.env.DO_MORE_PROMOTIONS ? process.env.DO_MORE_PROMOTIONS === 'true' : config.workers.doMorePromotions;
config.workers.doPunchCards = process.env.DO_PUNCH_CARDS ? process.env.DO_PUNCH_CARDS === 'true' : config.workers.doPunchCards;
config.workers.doDesktopSearch = process.env.DO_DESKTOP_SEARCH ? process.env.DO_DESKTOP_SEARCH === 'true' : config.workers.doDesktopSearch;
config.workers.doMobileSearch = process.env.DO_MOBILE_SEARCH ? process.env.DO_MOBILE_SEARCH === 'true' : config.workers.doMobileSearch;
config.searchSettings.useGeoLocaleQueries = process.env.USE_GEO_LOCALE_QUERIES ? process.env.USE_GEO_LOCALE_QUERIES === 'true' : config.searchSettings.useGeoLocaleQueries;
config.searchSettings.scrollRandomResults = process.env.SCROLL_RANDOM_RESULTS ? process.env.SCROLL_RANDOM_RESULTS === 'true' : config.searchSettings.scrollRandomResults;
config.searchSettings.clickRandomResults = process.env.CLICK_RANDOM_RESULTS ? process.env.CLICK_RANDOM_RESULTS === 'true' : config.searchSettings.clickRandomResults;
config.searchSettings.searchDelay.min = process.env.SEARCH_DELAY_MIN ? parseInt(process.env.SEARCH_DELAY_MIN, 10) : config.searchSettings.searchDelay.min;
config.searchSettings.searchDelay.max = process.env.SEARCH_DELAY_MAX ? parseInt(process.env.SEARCH_DELAY_MAX, 10) : config.searchSettings.searchDelay.max;
config.searchSettings.retryMobileSearch = process.env.RETRY_MOBILE_SEARCH ? process.env.RETRY_MOBILE_SEARCH === 'true' : config.searchSettings.retryMobileSearch;
config.webhook.enabled = process.env.WEBHOOK_ENABLED ? process.env.WEBHOOK_ENABLED === 'true' : config.webhook.enabled;
config.webhook.url = process.env.WEBHOOK_URL || config.webhook.url;
// Write the updated config back to the file
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log('Config file updated with environment variables');
} catch (error) {
console.error(`Failed to write updated config file to ${configPath}:`, error);
process.exit(1);
}

49
src/util/Axios.ts Normal file
View File

@@ -0,0 +1,49 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent'
import { AccountProxy } from '../interface/Account'
class AxiosClient {
private instance: AxiosInstance
private account: AccountProxy
constructor(account: AccountProxy) {
this.account = account
this.instance = axios.create()
// If a proxy configuration is provided, set up the agent
if (this.account.url && this.account.proxyAxios) {
const agent = this.getAgentForProxy(this.account)
this.instance.defaults.httpAgent = agent
this.instance.defaults.httpsAgent = agent
}
}
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
const { url, port } = proxyConfig
switch (true) {
case proxyConfig.url.startsWith('http'):
return new HttpProxyAgent(`${url}:${port}`)
case proxyConfig.url.startsWith('https'):
return new HttpsProxyAgent(`${url}:${port}`)
case proxyConfig.url.startsWith('socks'):
return new SocksProxyAgent(`${url}:${port}`)
default:
throw new Error(`Unsupported proxy protocol: ${url}`)
}
}
// Generic method to make any Axios request
public async request(config: AxiosRequestConfig, bypassProxy = false): Promise<AxiosResponse> {
if (bypassProxy) {
const bypassInstance = axios.create()
return bypassInstance.request(config)
}
return this.instance.request(config)
}
}
export default AxiosClient

View File

@@ -1,12 +1,13 @@
import { BrowserContext, Cookie } from 'playwright' import { BrowserContext, Cookie } from 'rebrowser-playwright'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator' import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { Account } from '../interface/Account' import { Account } from '../interface/Account'
import { Config } from '../interface/Config' import { Config, ConfigSaveFingerprint } from '../interface/Config'
let configCache: Config
export function loadAccounts(): Account[] { export function loadAccounts(): Account[] {
try { try {
@@ -28,16 +29,23 @@ export function loadAccounts(): Account[] {
export function loadConfig(): Config { export function loadConfig(): Config {
try { try {
if (configCache) {
return configCache
}
const configDir = path.join(__dirname, '../', 'config.json') const configDir = path.join(__dirname, '../', 'config.json')
const config = fs.readFileSync(configDir, 'utf-8') const config = fs.readFileSync(configDir, 'utf-8')
return JSON.parse(config) const configData = JSON.parse(config)
configCache = configData // Set as cache
return configData
} catch (error) { } catch (error) {
throw new Error(error as string) throw new Error(error as string)
} }
} }
export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, getFingerprint: boolean) { export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, saveFingerprint: ConfigSaveFingerprint) {
try { try {
// Fetch cookie file // Fetch cookie file
const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`) const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`)
@@ -52,7 +60,7 @@ export async function loadSessionData(sessionPath: string, email: string, isMobi
const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`) const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
let fingerprint!: BrowserFingerprintWithHeaders let fingerprint!: BrowserFingerprintWithHeaders
if (getFingerprint && fs.existsSync(fingerprintFile)) { if (((saveFingerprint.desktop && !isMobile) || (saveFingerprint.mobile && isMobile)) && fs.existsSync(fingerprintFile)) {
const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8') const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8')
fingerprint = JSON.parse(fingerprintData) fingerprint = JSON.parse(fingerprintData)
} }

View File

@@ -1,26 +1,45 @@
import chalk from 'chalk'
import { Webhook } from './Webhook' import { Webhook } from './Webhook'
import { loadConfig } from './Load'
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): void {
const configData = loadConfig()
if (configData.logExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
return
}
export function log(title: string, message: string, type?: 'log' | 'warn' | 'error') {
const currentTime = new Date().toLocaleString() const currentTime = new Date().toLocaleString()
const platformText = isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP'
const chalkedPlatform = isMobile === 'main' ? chalk.bgCyan('MAIN') : isMobile ? chalk.bgBlue('MOBILE') : chalk.bgMagenta('DESKTOP')
let str = '' // Clean string for the Webhook (no chalk)
const cleanStr = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`
// Send the clean string to the Webhook
if (!configData.webhookLogExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
Webhook(configData, cleanStr)
}
// Formatted string with chalk for terminal logging
const str = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${chalkedPlatform} [${title}] ${message}`
const applyChalk = color && typeof chalk[color] === 'function' ? chalk[color] as (msg: string) => string : null
// Log based on the type
switch (type) { switch (type) {
case 'warn': case 'warn':
str = `[${currentTime}] [PID: ${process.pid}] [WARN] [${title}] ${message}` applyChalk ? console.warn(applyChalk(str)) : console.warn(str)
console.warn(str)
break break
case 'error': case 'error':
str = `[${currentTime}] [PID: ${process.pid}] [ERROR] [${title}] ${message}` applyChalk ? console.error(applyChalk(str)) : console.error(str)
console.error(str)
break break
default: default:
str = `[${currentTime}] [PID: ${process.pid}] [LOG] [${title}] ${message}` applyChalk ? console.log(applyChalk(str)) : console.log(str)
console.log(str)
break break
} }
if (str) Webhook(str)
} }

View File

@@ -1,42 +1,45 @@
import axios from 'axios' import axios from 'axios'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import { log } from './Logger' import { log } from './Logger'
import { ChromeVersion, EdgeVersion } from '../interface/UserAgentUtil' import { ChromeVersion, EdgeVersion } from '../interface/UserAgentUtil'
export async function getUserAgent(mobile: boolean) { const NOT_A_BRAND_VERSION = '99'
const system = getSystemComponents(mobile)
const app = await getAppComponents(mobile)
const uaTemplate = mobile ? export async function getUserAgent(isMobile: boolean) {
const system = getSystemComponents(isMobile)
const app = await getAppComponents(isMobile)
const uaTemplate = isMobile ?
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Mobile Safari/537.36 EdgA/${app.edge_version}` : `Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Mobile Safari/537.36 EdgA/${app.edge_version}` :
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Safari/537.36 Edg/${app.edge_version}` `Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Safari/537.36 Edg/${app.edge_version}`
const platformVersion = `${mobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0` const platformVersion = `${isMobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0`
const uaMetadata = { const uaMetadata = {
mobile, isMobile,
platform: mobile ? 'Android' : 'Windows', platform: isMobile ? 'Android' : 'Windows',
fullVersionList: [ fullVersionList: [
{ brand: 'Not/A)Brand', version: '99.0.0.0' }, { brand: 'Not/A)Brand', version: `${NOT_A_BRAND_VERSION}.0.0.0` },
{ brand: 'Microsoft Edge', version: app['edge_version'] }, { brand: 'Microsoft Edge', version: app['edge_version'] },
{ brand: 'Chromium', version: app['chrome_version'] } { brand: 'Chromium', version: app['chrome_version'] }
], ],
brands: [ brands: [
{ brand: 'Not/A)Brand', version: '99' }, { brand: 'Not/A)Brand', version: NOT_A_BRAND_VERSION },
{ brand: 'Microsoft Edge', version: app['edge_major_version'] }, { brand: 'Microsoft Edge', version: app['edge_major_version'] },
{ brand: 'Chromium', version: app['chrome_major_version'] } { brand: 'Chromium', version: app['chrome_major_version'] }
], ],
platformVersion, platformVersion,
architecture: mobile ? '' : 'x86', architecture: isMobile ? '' : 'x86',
bitness: mobile ? '' : '64', bitness: isMobile ? '' : '64',
model: '' model: ''
} }
return { userAgent: uaTemplate, userAgentMetadata: uaMetadata } return { userAgent: uaTemplate, userAgentMetadata: uaMetadata }
} }
export async function getChromeVersion(): Promise<string> { export async function getChromeVersion(isMobile: boolean): Promise<string> {
try { try {
const request = { const request = {
url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json', url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
@@ -51,11 +54,11 @@ export async function getChromeVersion(): Promise<string> {
return data.channels.Stable.version return data.channels.Stable.version
} catch (error) { } catch (error) {
throw log('USERAGENT-CHROME-VERSION', 'An error occurred:' + error, 'error') throw log(isMobile, 'USERAGENT-CHROME-VERSION', 'An error occurred:' + error, 'error')
} }
} }
export async function getEdgeVersions() { export async function getEdgeVersions(isMobile: boolean) {
try { try {
const request = { const request = {
url: 'https://edgeupdates.microsoft.com/api/products', url: 'https://edgeupdates.microsoft.com/api/products',
@@ -75,7 +78,7 @@ export async function getEdgeVersions() {
} catch (error) { } catch (error) {
throw log('USERAGENT-EDGE-VERSION', 'An error occurred:' + error, 'error') throw log(isMobile, 'USERAGENT-EDGE-VERSION', 'An error occurred:' + error, 'error')
} }
} }
@@ -90,16 +93,18 @@ export function getSystemComponents(mobile: boolean): string {
return `${uaPlatform}; ${osId}` return `${uaPlatform}; ${osId}`
} }
export async function getAppComponents(mobile: boolean) { export async function getAppComponents(isMobile: boolean) {
const versions = await getEdgeVersions() const versions = await getEdgeVersions(isMobile)
const edgeVersion = mobile ? versions.android : versions.windows as string const edgeVersion = isMobile ? versions.android : versions.windows as string
const edgeMajorVersion = edgeVersion?.split('.')[0] const edgeMajorVersion = edgeVersion?.split('.')[0]
const chromeVersion = await getChromeVersion() const chromeVersion = await getChromeVersion(isMobile)
const chromeMajorVersion = chromeVersion?.split('.')[0] const chromeMajorVersion = chromeVersion?.split('.')[0]
const chromeReducedVersion = `${chromeMajorVersion}.0.0.0` const chromeReducedVersion = `${chromeMajorVersion}.0.0.0`
return { return {
not_a_brand_version: `${NOT_A_BRAND_VERSION}.0.0.0`,
not_a_brand_major_version: NOT_A_BRAND_VERSION,
edge_version: edgeVersion as string, edge_version: edgeVersion as string,
edge_major_version: edgeMajorVersion as string, edge_major_version: edgeMajorVersion as string,
chrome_version: chromeVersion as string, chrome_version: chromeVersion as string,
@@ -107,3 +112,32 @@ export async function getAppComponents(mobile: boolean) {
chrome_reduced_version: chromeReducedVersion as string chrome_reduced_version: chromeReducedVersion as string
} }
} }
export async function updateFingerprintUserAgent(fingerprint: BrowserFingerprintWithHeaders, isMobile: boolean): Promise<BrowserFingerprintWithHeaders> {
try {
const userAgentData = await getUserAgent(isMobile)
const componentData = await getAppComponents(isMobile)
//@ts-expect-error Errors due it not exactly matching
fingerprint.fingerprint.navigator.userAgentData = userAgentData.userAgentMetadata
fingerprint.fingerprint.navigator.userAgent = userAgentData.userAgent
fingerprint.fingerprint.navigator.appVersion = userAgentData.userAgent.replace(`${fingerprint.fingerprint.navigator.appCodeName}/`, '')
fingerprint.headers['user-agent'] = userAgentData.userAgent
fingerprint.headers['sec-ch-ua'] = `"Microsoft Edge";v="${componentData.edge_major_version}", "Not=A?Brand";v="${componentData.not_a_brand_major_version}", "Chromium";v="${componentData.chrome_major_version}"`
fingerprint.headers['sec-ch-ua-full-version-list'] = `"Microsoft Edge";v="${componentData.edge_version}", "Not=A?Brand";v="${componentData.not_a_brand_version}", "Chromium";v="${componentData.chrome_version}"`
/*
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36 EdgA/129.0.0.0
sec-ch-ua-full-version-list: "Microsoft Edge";v="129.0.2792.84", "Not=A?Brand";v="8.0.0.0", "Chromium";v="129.0.6668.90"
sec-ch-ua: "Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
"Google Chrome";v="129.0.6668.90", "Not=A?Brand";v="8.0.0.0", "Chromium";v="129.0.6668.90"
*/
return fingerprint
} catch (error) {
throw log(isMobile, 'USER-AGENT-UPDATE', 'An error occurred:' + error, 'error')
}
}

View File

@@ -1,3 +1,5 @@
import ms from 'ms'
export default class Util { export default class Util {
async wait(ms: number): Promise<void> { async wait(ms: number): Promise<void> {
@@ -16,11 +18,9 @@ export default class Util {
} }
shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
const shuffledArray = array.slice() return array.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
shuffledArray.sort(() => Math.random() - 0.5) .map(({ value }) => value)
return shuffledArray
} }
randomNumber(min: number, max: number): number { randomNumber(min: number, max: number): number {
@@ -39,4 +39,12 @@ export default class Util {
return chunks return chunks
} }
stringToMs(input: string | number): number {
const milisec = ms(input.toString())
if (!milisec) {
throw new Error('The string provided cannot be parsed to a valid time! Use a format like "1 min", "1m" or "1 minutes"')
}
return milisec
}
} }

View File

@@ -1,10 +1,10 @@
import axios from 'axios' import axios from 'axios'
import { loadConfig } from './Load'
import { Config } from '../interface/Config'
export async function Webhook(content: string) { export async function Webhook(configData: Config, content: string) {
const webhook = loadConfig().webhook const webhook = configData.webhook
if (!webhook.enabled || webhook.url.length < 10) return if (!webhook.enabled || webhook.url.length < 10) return

View File

@@ -65,7 +65,8 @@
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/accounts.json", "src/accounts.json",
"src/config.json" "src/config.json",
"src/functions/queries.json"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"