24 Commits

Author SHA1 Message Date
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
TheNetsky
ce2a72ee36 1.4.8 2024-08-18 15:06:44 +02:00
CNOCM
755237caa1 Skip locked Promotions (#132) 2024-08-18 14:37:56 +02:00
HMCDAT
2b4cd505c0 Support passwordless auth (#129)
* support passwordless auth (using Authenticator app)

* update readme
2024-07-24 15:01:45 +02:00
Nworm
a39a861dab Update Search.ts (#127) 2024-07-11 14:43:26 +02:00
mgrimace
8d19129906 Docker: improved env var handling (#113)
* Improve env var handling, clarify instructions

updateConfig.js will update dist/config.json with any values specified in the docker compose file as environmental variables (env vars). If not specified it will use the default values in src/config.json (the 'usual' place where folks can customize their config).

A user can make changes to an env var (e.g., disabling Scroll Random Results), then docker compose up -d to quickly restart the container with the change.

* minor update to env vars in table

Make sure to change your compose so the updated flattened values work.

* TZ handling for cron runs of the script

docker logs netsky should now show the proper time zone for script runs that were initiated via cron schedule.
2024-06-01 14:50:29 +02:00
TheNetsky
c6ab80fe54 Delete start.bat
oops
2024-05-28 11:39:34 +02:00
TheNetsky
9b1eed526f 1.4.7
- Updated packages
- Added timeout after scrolling back to top
- Some Docker improvement thanks to @mgrimace
2024-05-28 11:38:49 +02:00
mgrimace
9a144b2e60 Docker with env var support and containerized cron scheduling (#107)
* Script will run on container start, and continue to run daily at 5:00 am.

The goal of this update was to add in-built cronjob support so that the script runs immediately on container start, then will schedule the script to run the daily at 5:00 am without requiring the user to manually create a cronjob. After starting the container and completing the first run, the container should remain active (running) but idle until the next scheduled run. Ideally, the next step will be to include support for environmental variables that will allow the user to customize when the script scheduled runs occur within the docker compose.yaml.

* added --build to docker compose command instructions

building/rebuilding the image (and the script) may reduce problems if folks make changes to their configs

* Updated the crontab default to run twice daily by default, 5am and 11am

* Update crontab

Newline needed

* Environmental variable support, custom scheduling

This is a massive update that includes docker environmental variable support for all config options. If specified, the custom env var value will supersede the default values in the config.json. You do not need to change the values in the config.json if you set a custom env var. For example, if you have headless=false in the config.json, but set it as true in the compose.yaml, it will run as HEADLESS=true. If you do not specify a particular env var, the script will automatically use the default value. Please test and contribute wherever possible!

* Fix scheduled runs, tidied up readme

* timezone support

Minor adjustment to attempt to fix timezone env var support

* Run on start, npm script env

Hopefully two fixes here, the container should again, by default, run on start, then continue per schedule. This defaults to true; however, can be optionally over-ridden in the compose.yaml with the env var RUN_ON_START=false. If set to false, the script should only run on the specified cron schedule. I also passed the TZ write into the npm start command, rather than just the container overall so it should show the proper time.

* Keep docker running after run_on_start to wait for cron

Accidentally set it up like a run_once, this should keep the container active after run_on_start to wait for the next schedule.

* improve order of cmd

This should (1) set time zone for script, (2) run once if set, then (3) idle for scheduled cron job. It was a little hacky before, this should be tidier.

* updated compose to auto-restart container

Noticed the container was exited this am, likely after automated updated to system, adding restart: unless-stopped to compose should keep container running in such circumstances.

* Update env vars in readme
2024-05-16 19:28:55 +02:00
Netsky
28b1881642 Update README.md 2024-05-09 12:38:49 +02:00
TheNetsky
ef6ad569ff 1.4.6 2024-05-09 10:04:11 +02:00
Netsky
da9ba91c5c Merge pull request #98 from jordyamc/main
Bug when detecting earnable points from more promotions
2024-05-07 15:12:10 +02:00
Jordy Mendoza
deb2d58b1b - Fix bug where the script was using x.activityType instead of x.promotionType this was causing that the script wasn't detecting earnable points from more promotions 2024-04-21 23:50:33 -06:00
Netsky
66a82c2584 Merge pull request #89 from mgrimace/docker
Clarify docker instructions, add basic compose.yaml support
2024-04-04 16:43:57 +02:00
mgrimace
8a022d5983 Clarify docker instructions, add basic compose.yaml support
This should help folks get started with docker and hopefully make it easier for testing
2024-04-03 17:59:16 -04:00
TheNetsky
64048e35d7 1.4.5
- Fix login not working
- Sessions are now saved after logging in
- Check if promotions, and searches are available before counting total point amount
2024-03-01 12:19:48 +01:00
TheNetsky
cf7f7ac790 Merge branch 'main' of https://github.com/TheNetsky/Microsoft-Rewards-Script 2024-02-05 11:47:50 +01:00
TheNetsky
f7aa5039f9 1.4.4
- Fixed login getting stuck
- Updated packages
- Fixed some errors not throwing
2024-02-05 11:47:48 +01:00
Netsky
e082fb03f0 Merge pull request #56 from Omniphantasm/patch-1
Update README.md
2024-01-26 18:55:10 +01:00
Omniphantasm
0303b8c605 Update README.md
Script is kill-chrome-win, changed readme to reflect that.
2024-01-26 12:35:30 -05:00
TheNetsky
2fea17c415 1.4.3
- Fixed browser generating mobile browser after the first account
- Hopefully fixed script erroring after not finding parent promotions
- Added support for MacOS (Thanks @alecm858)
- Made some changes to search result scrolling
2024-01-20 12:11:02 +01:00
Netsky
c5beccb54b Update README.md 2024-01-17 22:23:55 +01:00
TheNetsky
b566ccaece 1.4.2
- Scroll back to top before searching
2024-01-17 18:50:43 +01:00
24 changed files with 893 additions and 156 deletions

View File

@@ -28,6 +28,10 @@ module.exports = {
'error', 'error',
'never' 'never'
], ],
'@typescript-eslint/no-explicit-any':
['warn', {
fixToUnknown: true // This line is optional and only relevant if you are using TypeScript
}],
'comma-dangle': 'off', 'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'error', '@typescript-eslint/comma-dangle': 'error',
'prefer-arrow-callback': 'error' 'prefer-arrow-callback': 'error'

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ accounts.json
notes notes
accounts.dev.json accounts.dev.json
accounts.main.json accounts.main.json
.DS_Store

View File

@@ -4,20 +4,12 @@ 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 # Install jq, cron, and gettext-base
RUN apt-get update && apt-get install -y jq RUN apt-get update && apt-get install -y jq cron gettext-base
# Copy all files to the working directory # Copy all files to the working directory
COPY . . COPY . .
# Check if "headless" is set to "true" in the config.json file
RUN HEADLESS=$(cat src/config.json | jq -r .headless) \
&& if [ "$HEADLESS" != "true" ]; then \
echo "Error: 'headless' in src/config.json is not true."; \
exit 1; \
fi
# Install dependencies including Playwright # Install dependencies including Playwright
RUN apt-get install -y \ RUN apt-get install -y \
xvfb \ xvfb \
@@ -38,5 +30,11 @@ RUN npm run build
# Install playwright chromium # Install playwright chromium
RUN npx playwright install chromium RUN npx playwright install chromium
# Define the command to run your application # Copy cron file to cron directory
CMD ["npm", "start"] COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
# Create the log file to be able to run tail
RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally
CMD sh -c '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'

View File

@@ -12,36 +12,78 @@ Under development, however mainly for personal use!
6. Run `npm run start` to start the built script 6. Run `npm run start` to start the built script
## Notes ## ## Notes ##
- If you end the script without closing the browser window first (only with headless as false), you'll be left with hanging chrome instances using resources. Use taskmanager to kill these or use the included `npm run chrome-kill-win` script. (Windows) - If you end the script without closing the browser window first (only with headless as false), you'll be left with hanging chrome instances using resources. Use taskmanager to kill these or use the included `npm run kill-chrome-win` script. (Windows)
- If you automate this script, set it to run at least 2 times a day to make sure it picked up all tasks, set `"runOnZeroPoints": false` so it doesn't run when no points are found. - 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 container has to be recreated for any changes regardings the `config.json` and/or `accounts.json`.
## 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.
1. Download the source code
2. Make changes to your `accounts.json` and `config.json`
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
1. Build or re-build the container image with: `docker build -t microsoft-rewards-script-docker .`
2. Run the container with:
```bash
docker run --name netsky -d \
-e TZ=America/New_York \
-e HEADLESS=true \
-e RUN_ON_START=true \
-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.
4. You can view logs with `docker logs netsky`.
### Option 2: use docker compose
1. A basic docker compose.yaml has been provided.
2. Optionally, customize your config by adding any other environmental variables from the table below.
3. Build and start the container using `docker compose up -d`.
4. You can view logs with `docker logs netsky`
## Config ## ## Config ##
| Setting | Description | Default | | Setting | Description | Default | Docker Environmental Variable |
| :------------- |:-------------| :-----| | :------------- |:-------------| :-----| :-----|
| baseURL | MS Rewards page | `https://rewards.bing.com` | | baseURL | MS Rewards page | `https://rewards.bing.com` | BASE_URL |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In src/browser/sessions) | | sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) | SESSION_PATH |
| headless | If the browser window should be visable be ran in the background | `false` (Browser is visable) | | 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)* |
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | | runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | RUN_ON_ZERO_POINTS |
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) | | clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) | CLUSTERS |
| saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | | saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | SAVE_FINGERPRINT |
| workers.doDailySet | Complete daily set items | `true` | | workers.doDailySet | Complete daily set items | `true` | DO_DAILY_SET |
| workers.doMorePromotions | Complete promotional items | `true` | | workers.doMorePromotions | Complete promotional items | `true` | DO_MORE_PROMOTIONS |
| workers.doPunchCards | Complete punchcards | `true` | | workers.doPunchCards | Complete punchcards | `true` | DO_PUNCH_CARDS |
| workers.doDesktopSearch | Complete daily desktop searches | `true` | | workers.doDesktopSearch | Complete daily desktop searches | `true` | DO_DESKTOP_SEARCH |
| workers.doMobileSearch | Complete daily mobile searches | `true` | | workers.doMobileSearch | Complete daily mobile searches | `true` | DO_MOBILE_SEARCH |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | | globalTimeout | The length before the action gets timeout | `30000` (30 seconds) | GLOBAL_TIMEOUT |
| scrollRandomResults | Scroll randomly in search results | `true` | | searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | USE_GEO_LOCALE_QUERIES |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | | scrollRandomResults | Scroll randomly in search results | `true` | SCROLL_RANDOM_RESULTS |
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 10000` (10 seconds) `max: 20000` (20 seconds) | | searchSettings.clickRandomResults | Visit random website from search result| `true` | CLICK_RANDOM_RESULTS |
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | | 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 |
| webhook.enabled | Enable or disable your set webhook | `false` | | searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | RETRY_MOBILE_SEARCH |
| webhook.url | Your Discord webhook URL | `null` | | webhook.enabled | Enable or disable your set webhook | `false` | WEBHOOK_ENABLED |
| webhook.url | Your Discord webhook URL | `null` | WEBHOOK_URL="" |
| cronStartTime | Scheduled script run-time, *only available for docker implementation* | `0 5,11 * * *` (5:00 am, 11:00 am daily) | CRON_START_TIME="" |
| | Run the script immediately when the Docker container starts | `true` | RUN_ON_START |
## Features ## ## Features ##
- [x] Multi-Account Support - [x] Multi-Account Support
- [x] Session Storing - [x] Session Storing
- [x] 2FA Support - [x] 2FA Support
- [x] Passwordless Support
- [x] Headless Support - [x] Headless Support
- [x] Discord Webhook Support - [x] Discord Webhook Support
- [x] Desktop Searches - [x] Desktop Searches
@@ -60,10 +102,14 @@ 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
- [x] Completing Daily Check In
- [x] Completing Read To Earn
- [ ] Completing Shopping Game - [ ] Completing Shopping Game
- [ ] Completing Gaming Tab - [ ] Completing Gaming Tab
- [x] Clustering Support - [x] Clustering Support
- [x] Proxy Support - [x] Proxy Support
- [x] Docker Support (experimental)
- [x] Automatic scheduling (via Docker)
## Disclaimer ## ## Disclaimer ##
Your account may be at risk of getting banned or suspended using this script, you've been warned! Your account may be at risk of getting banned or suspended using this script, you've been warned!

13
compose.yaml Normal file
View File

@@ -0,0 +1,13 @@
services:
microsoft-rewards-script:
build: .
container_name: netsky
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

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "1.4.1", "version": "1.4.9",
"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": {
@@ -12,7 +12,7 @@
"ts-start": "ts-node ./src/index.ts", "ts-start": "ts-node ./src/index.ts",
"dev": "ts-node ./src/index.ts -dev", "dev": "ts-node ./src/index.ts -dev",
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"", "kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
"create-docker" : "docker build -t microsoft-rewards-script-docker ." "create-docker": "docker build -t microsoft-rewards-script-docker ."
}, },
"keywords": [ "keywords": [
"Bing Rewards", "Bing Rewards",
@@ -26,17 +26,17 @@
"author": "Netsky", "author": "Netsky",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.54.0", "eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.3.3" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.2", "axios": "^1.7.5",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.46", "fingerprint-generator": "^2.1.54",
"fingerprint-injector": "^2.1.46", "fingerprint-injector": "^2.1.54",
"playwright": "^1.40.1", "playwright": "^1.46.1",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
} }

View File

@@ -43,6 +43,9 @@ class Browser {
const context = await newInjectedContext(browser, { fingerprint: fingerpint }) const context = await newInjectedContext(browser, { fingerprint: fingerpint })
// Set timeout to preferred amount
context.setDefaultTimeout(this.bot.config?.globalTimeout ?? 30_000)
await context.addCookies(sessionData.cookies) await context.addCookies(sessionData.cookies)
if (this.bot.config.saveFingerprint) { if (this.bot.config.saveFingerprint) {

View File

@@ -1,10 +1,12 @@
import { Page } from 'playwright' import { Page } from 'playwright'
import { CheerioAPI, load } from 'cheerio' import { CheerioAPI, load } from 'cheerio'
import axios, { AxiosRequestConfig } from 'axios'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
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'
export default class BrowserFunc { export default class BrowserFunc {
@@ -95,13 +97,13 @@ export default class BrowserFunc {
const scripts = Array.from(document.querySelectorAll('script')) const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard')) const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
if (targetScript) { return targetScript?.innerText ? targetScript.innerText : null
return targetScript.innerText
} else {
throw this.bot.log('GET-DASHBOARD-DATA', 'Script containing dashboard data not found', 'error')
}
}) })
if (!scriptContent) {
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
}
// Extract the dashboard object from the script content // Extract the dashboard object from the script content
const dashboardData = await this.bot.homePage.evaluate(scriptContent => { const dashboardData = await this.bot.homePage.evaluate(scriptContent => {
// Extract the dashboard object using regex // Extract the dashboard object using regex
@@ -110,11 +112,13 @@ export default class BrowserFunc {
if (match && match[1]) { if (match && match[1]) {
return JSON.parse(match[1]) return JSON.parse(match[1])
} else {
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
} }
}, scriptContent) }, scriptContent)
if (!dashboardData) {
throw this.bot.log('GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
}
return dashboardData return dashboardData
} }
@@ -129,10 +133,10 @@ 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<number> {
try { try {
const data = await this.getDashboardData() const data = await this.getDashboardData()
@@ -140,7 +144,9 @@ export default class BrowserFunc {
let totalEarnablePoints = 0 let totalEarnablePoints = 0
// Desktop Search Points // Desktop Search Points
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) if (data.userStatus.counters.pcSearch?.length) {
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
}
// Mobile Search Points // Mobile Search Points
if (data.userStatus.counters.mobileSearch?.length) { if (data.userStatus.counters.mobileSearch?.length) {
@@ -151,16 +157,68 @@ export default class BrowserFunc {
data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
// More Promotions // More Promotions
data.morePromotions.forEach(x => { if (data.morePromotions?.length) {
// Only count points from supported activities data.morePromotions.forEach(x => {
if (['quiz', 'urlreward'].includes(x.activityType)) { // Only count points from supported activities
totalEarnablePoints += (x.pointProgressMax - x.pointProgress) if (['quiz', 'urlreward'].includes(x.promotionType) && !x.attributes.is_unlocked) {
} totalEarnablePoints += (x.pointProgressMax - x.pointProgress)
}) }
})
}
return totalEarnablePoints return totalEarnablePoints
} catch (error) { } catch (error) {
throw this.bot.log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error') throw this.bot.log('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): Promise<number> {
try {
const eligibleOffers = [
'ENUS_readarticle3_30points',
'Gamification_Sapphire_DailyCheckIn'
]
let totalEarnablePoints = 0
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 axios(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') {
totalEarnablePoints += 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()) {
totalEarnablePoints += parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '')
}
break
}
}
return totalEarnablePoints
} catch (error) {
throw this.bot.log('GET-APP-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
} }
} }

View File

@@ -12,6 +12,7 @@ export default class BrowserUtil {
async tryDismissAllMessages(page: Page): Promise<boolean> { async tryDismissAllMessages(page: Page): Promise<boolean> {
const buttons = [ const buttons = [
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' }, { selector: '#iLandingViewAction', label: 'iLandingViewAction' },
{ selector: '#iShowSkip', label: 'iShowSkip' }, { selector: '#iShowSkip', label: 'iShowSkip' },
{ selector: '#iNext', label: 'iNext' }, { selector: '#iNext', label: 'iNext' },

View File

@@ -10,8 +10,11 @@
"doMorePromotions": true, "doMorePromotions": true,
"doPunchCards": true, "doPunchCards": true,
"doDesktopSearch": true, "doDesktopSearch": true,
"doMobileSearch": true "doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true
}, },
"globalTimeout": 30000,
"searchSettings": { "searchSettings": {
"useGeoLocaleQueries": false, "useGeoLocaleQueries": false,
"scrollRandomResults": true, "scrollRandomResults": true,
@@ -25,5 +28,6 @@
"webhook": { "webhook": {
"enabled": false, "enabled": false,
"url": "" "url": ""
} },
"cronStartTime": "0 5,11 * * *"
} }

1
src/crontab.template Normal file
View File

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

View File

@@ -8,6 +8,8 @@ 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 { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData } from '../interface/DashboardData' import { DashboardData } from '../interface/DashboardData'
@@ -49,4 +51,14 @@ export default class Activities {
await urlReward.doUrlReward(page) await urlReward.doUrlReward(page)
} }
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

@@ -2,6 +2,10 @@ import { Page } from 'playwright'
import readline from 'readline' import readline from 'readline'
import { MicrosoftRewardsBot } from '../index' import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
import axios from 'axios'
import { OAuth } from '../interface/OAuth'
import * as crypto from 'crypto'
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
@@ -11,6 +15,11 @@ const rl = readline.createInterface({
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
@@ -26,14 +35,12 @@ export class Login {
if (!isLoggedIn) { if (!isLoggedIn) {
// Check if account is locked // Check if account is locked
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 10_000 }).then(() => true).catch(() => false) const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
if (isLocked) { if (isLocked) {
this.bot.log('LOGIN', 'This account has been locked!', 'error') this.bot.log('LOGIN', 'This account has been locked!', 'error')
throw new Error('Account has been locked!') throw new Error('Account has been locked!')
} }
await page.waitForSelector('#loginHeader', { state: 'visible', timeout: 10_000 })
await this.execLogin(page, email, password) await this.execLogin(page, email, password)
this.bot.log('LOGIN', 'Logged into Microsoft successfully') this.bot.log('LOGIN', 'Logged into Microsoft successfully')
} else { } else {
@@ -43,6 +50,9 @@ export class Login {
// Check if logged in to bing // Check if logged in to bing
await this.checkBingLogin(page) await this.checkBingLogin(page)
// Save session
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('LOGIN', 'Logged in successfully')
@@ -53,46 +63,111 @@ export class Login {
} }
private async execLogin(page: Page, email: string, password: string) { private async execLogin(page: Page, email: string, password: string) {
await page.type('#i0116', email) try {
await this.enterEmail(page, email)
await this.enterPassword(page, password)
await this.checkLoggedIn(page)
} catch (error: any) {
this.bot.log('LOGIN', 'An error occurred: ' + error.message, 'error')
}
}
private async enterEmail(page: Page, email: string) {
await page.fill('#i0116', email)
await page.click('#idSIButton9') await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully') this.bot.log('LOGIN', 'Email entered successfully')
}
private async enterPassword(page: Page, password: string) {
try { try {
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.fill('#i0118', password)
await page.type('#i0118', password)
await page.click('#idSIButton9') await page.click('#idSIButton9')
// When erroring at this stage it means a 2FA code is required
} catch (error) {
this.bot.log('LOGIN', '2FA code required')
// Wait for user input
const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.type('input[name="otc"]', code)
await page.keyboard.press('Enter')
} finally {
this.bot.log('LOGIN', 'Password entered successfully') this.bot.log('LOGIN', 'Password entered successfully')
} catch {
this.bot.log('LOGIN', 'Password entry failed or 2FA required')
await this.handle2FA(page)
} }
}
const currentURL = new URL(page.url()) private async handle2FA(page: Page) {
try {
const numberToPress = await this.get2FACode(page)
if (numberToPress) {
// Authentictor App verification
await this.authAppVerification(page, numberToPress)
} else {
// SMS verification
await this.authSMSVerification(page)
}
} catch (error: any) {
this.bot.log('LOGIN', `2FA handling failed: ${error.message}`)
}
}
while (currentURL.pathname !== '/' || currentURL.hostname !== 'rewards.bing.com') { private async get2FACode(page: Page): Promise<string | null> {
try {
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent()
} catch {
await page.click('button[aria-describedby="confirmSendTitle"]')
await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent()
}
}
private async authAppVerification(page: Page, numberToPress: string | null) {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
this.bot.log('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')
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
this.bot.log('LOGIN', 'Login successfully approved!')
break
} catch {
this.bot.log('LOGIN', 'The code is expired. Trying to get a new code...')
await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]')
numberToPress = await this.get2FACode(page)
}
}
}
private async authSMSVerification(page: Page) {
this.bot.log('LOGIN', 'SMS 2FA code required. Waiting for user input...')
const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
this.bot.log('LOGIN', '2FA code entered successfully')
}
private async checkLoggedIn(page: Page) {
const targetHostname = 'rewards.bing.com'
const targetPathname = '/'
// eslint-disable-next-line no-constant-condition
while (true) {
await this.bot.browser.utils.tryDismissAllMessages(page) await this.bot.browser.utils.tryDismissAllMessages(page)
currentURL.href = page.url() const currentURL = new URL(page.url())
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
break
}
} }
// 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: 10_000 })
this.bot.log('LOGIN', 'Successfully logged into the rewards portal')
} }
private async checkBingLogin(page: Page): Promise<void> { private async checkBingLogin(page: Page): Promise<void> {
@@ -133,4 +208,50 @@ 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)
const currentUrl = new URL(page.url())
let code: string
// eslint-disable-next-line no-constant-condition
while (true) {
this.bot.log('LOGIN-APP', 'Waiting for authorization')
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
code = currentUrl.searchParams.get('code')!
break
}
}
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 = {
url: this.tokenUrl,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body.toString()
}
const tokenResponse = await axios(tokenRequest)
const tokenData: OAuth = await tokenResponse.data
this.bot.log('LOGIN-APP', 'Successfully authorized')
return tokenData.access_token
}
} }

View File

@@ -38,7 +38,7 @@ export class Workers {
// 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?.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('PUNCH-CARD', 'All "Punch Cards" have already been completed')
@@ -58,7 +58,7 @@ export class Workers {
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)
@@ -87,7 +87,7 @@ 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) ?? [] const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && !x.attributes.is_unlocked) ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
@@ -132,13 +132,13 @@ export class Workers {
} }
let selector = `[data-bi-id="${activity.offerId}"]` let selector = `[data-bi-id^="${activity.offerId}"]`
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')) {
selector = `[data-bi-id="${activity.name}"]` selector = `[data-bi-id^="${activity.name}"]`
} }
// Click element, it will be opened in a new tab // Click element, it will be opened in a new tab

View File

@@ -0,0 +1,48 @@
import axios from 'axios'
import { randomBytes } from 'crypto'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class DailyCheckIn extends Workers {
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
this.bot.log('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 = {
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 axios(claimRequest)
const claimedPoint = parseInt((await claimResponse.data).response.activity.p)
this.bot.log('DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
} catch (error) {
this.bot.log('DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,73 @@
import axios from 'axios'
import { randomBytes } from 'crypto'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class ReadToEarn extends Workers {
public async doReadToEarn(accessToken: string, data: DashboardData) {
this.bot.log('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 = {
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 axios(userDataRequest)
const userData = (await userDataResponse.data).response
let balance: number = userData.balance
const jsonData = {
amount: 1,
country: geoLocale,
id: '1',
type: 101,
attributes: {
offerid: 'ENUS_readarticle3_30points'
}
}
for (let i = 0; i < 10; ++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 axios(claimRequest)
const newBalance = (await claimResponse.data).response.balance
if (newBalance == balance) {
this.bot.log('READ-TO-EARN', 'Read all available articles')
break
} else {
balance = newBalance
this.bot.log('READ-TO-EARN', `Read article ${i + 1}`)
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
}
}
this.bot.log('READ-TO-EARN', 'Completed Read to Earn')
} catch (error) {
this.bot.log('READ-TO-EARN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -1,5 +1,6 @@
import { Page } from 'playwright' import { Page } from 'playwright'
import axios from 'axios' import axios from 'axios'
import { platform } from 'os'
import { Workers } from '../Workers' import { Workers } from '../Workers'
@@ -41,6 +42,8 @@ export class Search extends Workers {
// 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
@@ -97,7 +100,7 @@ export class Search extends Workers {
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('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`)
searchCounters = await this.bingSearch(page, query.topic) searchCounters = await this.bingSearch(page, term)
const newMissingPoints = this.calculatePoints(searchCounters) const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before // If the new point amount is the same as before
@@ -128,30 +131,50 @@ export class Search extends Workers {
} }
private async bingSearch(searchPage: Page, query: string) { private async bingSearch(searchPage: Page, query: string) {
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
// 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 {
// Go to top of the page
await searchPage.evaluate(() => {
window.scrollTo(0, 0)
})
// Set it since params get added after visiting
this.searchPageURL = searchPage.url()
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q' const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'attached', timeout: 10_000 }) await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 })
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('Control') await searchPage.keyboard.down(platformControlKey)
await searchPage.keyboard.press('A') await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace') await searchPage.keyboard.press('Backspace')
await searchPage.keyboard.up('Control') await searchPage.keyboard.up(platformControlKey)
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(1000)
// Bing.com in Chrome opens a new tab when searching
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
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
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.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
return await this.bot.browser.func.getSearchPoints() return await this.bot.browser.func.getSearchPoints()
@@ -167,7 +190,7 @@ export class Search extends Workers {
// 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)
} }
@@ -248,10 +271,13 @@ export class Search extends Workers {
private async randomScroll(page: Page) { private async randomScroll(page: Page) {
try { try {
// Press the arrow down key to scroll const viewportHeight = await page.evaluate(() => window.innerHeight)
for (let i = 0; i < this.bot.utils.randomNumber(5, 600); i++) { const totalHeight = await page.evaluate(() => document.body.scrollHeight)
await page.keyboard.press('ArrowDown') const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
}
await page.evaluate((scrollPos) => {
window.scrollTo(0, scrollPos)
}, randomScrollPosition)
} catch (error) { } catch (error) {
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error') this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
@@ -260,23 +286,22 @@ export class Search extends Workers {
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').catch(() => { }) // Since we don't really care if it did it or not await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
// Will get current tab if no new one is created // Will get current tab if no new one is created
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 // Stay for 10 seconds
await this.bot.utils.wait(5000) await this.bot.utils.wait(10_000)
let lastTabURL = new URL(lastTab.url()) // Get new tab info 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
@@ -289,26 +314,25 @@ export class Search extends Workers {
} }
} }
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 // If more than 2 tabs are open, close the last tab
if (tabs.length > 2) { if (tabs.length > 2) {
await lastTab.close() await lastTab.close()
// If only 1 tab is open, open a new one to search in // If only 1 tab is open, open a new one to search in
} else if (tabs.length === 1) { } else if (tabs.length === 1) {
const newPage = await browser.newPage() const newPage = await browser.newPage()
await newPage.goto(url) await newPage.goto(this.searchPageURL)
// Else go back one page // Else go back one page, this means the correct amount is open
} else { } else {
await lastTab.goBack() await lastTab.goBack().catch(() => { })
} }
} }
private calculatePoints(counters: Counters) { private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches const genericData = counters.pcSearch?.[0] // Normal searches

View File

@@ -34,6 +34,7 @@ export class MicrosoftRewardsBot {
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() { constructor() {
this.log = log this.log = log
@@ -125,6 +126,8 @@ export class MicrosoftRewardsBot {
// 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()
@@ -132,14 +135,19 @@ export class MicrosoftRewardsBot {
// 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()
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`) log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
const earnablePoints = await this.browser.func.getEarnablePoints() const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
const appEarnablePoints = await this.browser.func.getAppEarnablePoints(this.accessToken)
const earnablePoints = browserEnarablePoints + appEarnablePoints
this.collectedPoints = earnablePoints this.collectedPoints = earnablePoints
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`) log('MAIN-POINTS', `You can earn ${earnablePoints} points today (Browser: ${browserEnarablePoints} points, App: ${appEarnablePoints} points)`)
// 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.collectedPoints === 0) {
@@ -176,11 +184,11 @@ export class MicrosoftRewardsBot {
} }
// Save cookies // Save cookies
const cookies = await browser.cookies() await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
await saveSessionData(this.config.sessionPath, account.email, this.isMobile, cookies)
// Close desktop browser // Close desktop browser
return await this.closeBrowser(browser, account.email) await this.closeBrowser(browser, account.email)
return
} }
// Mobile // Mobile
@@ -198,55 +206,62 @@ export class MicrosoftRewardsBot {
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) // Do daily check in
if (!data.userStatus.counters.mobileSearch) { if (this.config.workers.doDailyCheckIn) {
log('MAIN', 'No mobile searches found, stopping!') await this.activities.doDailyCheckIn(this.accessToken, data)
// Close mobile browser
return await this.closeBrowser(browser, account.email)
} }
// Open a new tab to where the tasks are going to be completed // Do read to earn
const workerPage = await browser.newPage() if (this.config.workers.doReadToEarn) {
await this.activities.doReadToEarn(this.accessToken, data)
}
// Go to homepage on worker page // If no mobile searches data found, stop (Does not exist on new accounts)
await this.browser.func.goHome(workerPage) if (data.userStatus.counters.mobileSearch) {
// Open a new tab to where the tasks are going to be completed
const workerPage = await browser.newPage()
// Do mobile searches // Go to homepage on worker page
if (this.config.workers.doMobileSearch) { await this.browser.func.goHome(workerPage)
await this.activities.doSearch(workerPage, data)
// Fetch current search points // Do mobile searches
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] if (this.config.workers.doMobileSearch) {
await this.activities.doSearch(workerPage, data)
// If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid // Fetch current search points
// Retry until all points are gathered when (retryMobileSearch is enabled) const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
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 // If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid
await this.closeBrowser(browser, account.email) // 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...')
// Retry // Close mobile browser
await this.Mobile(account) await this.closeBrowser(browser, account.email)
// Retry
await this.Mobile(account)
}
} }
} else {
log('MAIN', 'No mobile searches found!')
} }
// Fetch new points // Fetch new points
const earnablePoints = await this.browser.func.getEarnablePoints() const earnablePoints = await this.browser.func.getBrowserEarnablePoints() + await this.browser.func.getAppEarnablePoints(this.accessToken)
// If the new earnable is 0, means we got all the points, else retract // If the new earnable is 0, means we got all the points, else retract
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints) this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`) log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
// Close mobile browser // Close mobile browser
return await this.closeBrowser(browser, account.email) await this.closeBrowser(browser, account.email)
return
} }
private async closeBrowser(browser: BrowserContext, email: string) { private async closeBrowser(browser: BrowserContext, email: string) {
// Save cookies // Save cookies
const cookies = await browser.cookies() await saveSessionData(this.config.sessionPath, browser, email, this.isMobile)
await saveSessionData(this.config.sessionPath, email, this.isMobile, cookies)
// Close browser // Close browser
await browser.close() await browser.close()

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

@@ -5,6 +5,7 @@ export interface Config {
runOnZeroPoints: boolean; runOnZeroPoints: boolean;
clusters: number; clusters: number;
workers: Workers; workers: Workers;
globalTimeout: number;
searchSettings: SearchSettings; searchSettings: SearchSettings;
webhook: Webhook; webhook: Webhook;
saveFingerprint: boolean; saveFingerprint: boolean;
@@ -34,4 +35,6 @@ export interface Workers {
doPunchCards: boolean; doPunchCards: boolean;
doDesktopSearch: boolean; doDesktopSearch: boolean;
doMobileSearch: boolean; doMobileSearch: boolean;
doDailyCheckIn: boolean;
doReadToEarn: boolean;
} }

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;
}

32
src/run_daily.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Set up environment variables
export PATH=$PATH:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
# Ensure TZ is set
export TZ=${TZ}
# Change directory to the application directory
cd /usr/src/microsoft-rewards-script
# Define the minimum and maximum wait times in seconds
MINWAIT=$((5*60)) # 5 minutes
MAXWAIT=$((50*60)) # 50 minutes
# Calculate a random sleep time within the specified range
SLEEPTIME=$((MINWAIT + RANDOM % (MAXWAIT - MINWAIT)))
# Convert the sleep time to minutes for logging
SLEEP_MINUTES=$((SLEEPTIME / 60))
# Log the sleep duration
echo "Sleeping for $SLEEP_MINUTES minutes ($SLEEPTIME seconds)..."
# Sleep for the calculated time
sleep $SLEEPTIME
# Log the start of the script
echo "Starting script..."
# Execute the Node.js script directly
npm run start

43
src/updateConfig.js Executable file
View File

@@ -0,0 +1,43 @@
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.workers.doDailyCheckIn = process.env.DO_DAILY_CHECK_IN ? process.env.DO_DAILY_CHECK_IN === 'true' : config.workers.doDailyCheckIn
config.workers.doReadToEarn = process.env.DO_READ_TO_EARN ? process.env.DO_READ_TO_EARN === 'true' : config.workers.doReadToEarn
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)
}

View File

@@ -1,4 +1,4 @@
import { Cookie } from 'playwright' import { BrowserContext, Cookie } from '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'
@@ -67,8 +67,10 @@ export async function loadSessionData(sessionPath: string, email: string, isMobi
} }
} }
export async function saveSessionData(sessionPath: string, email: string, isMobile: boolean, cookies: Cookie[]): Promise<string> { export async function saveSessionData(sessionPath: string, browser: BrowserContext, email: string, isMobile: boolean): Promise<string> {
try { try {
const cookies = await browser.cookies()
// Fetch path // Fetch path
const sessionDir = path.join(__dirname, '../browser/', sessionPath, email) const sessionDir = path.join(__dirname, '../browser/', sessionPath, email)