7 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
20 changed files with 742 additions and 148 deletions

View File

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

View File

@@ -37,4 +37,4 @@ COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally
CMD sh -c '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 '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

@@ -16,11 +16,14 @@ 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.
## 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`
3. **Headless mode must be enabled when using Docker.** You can do this using the `HEADLESS=true` environmental variable in docker run or docker compose.yaml (see below). Environmental variables are always prioritized over the values in config.json.
4. The container will run scheduled. 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.
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 .`
@@ -31,14 +34,12 @@ Under development, however mainly for personal use!
docker run --name netsky -d \
-e TZ=America/New_York \
-e HEADLESS=true \
-e SEARCH_DELAY_MIN=10000 \
-e SEARCH_DELAY_MAX=20000 \
-e CLUSTERS=1 \
-e RUN_ON_START=true \
-e CRON_START_TIME="0 5,11 * * *" \
microsoft-rewards-script-docker
```
3. Optionally, change any environmental variables other than `HEADLESS`, which must stay `=true`
3. Optionally, customize your config by adding any other environmental variables from the table below.
4. You can view logs with `docker logs netsky`.
@@ -46,9 +47,9 @@ Under development, however mainly for personal use!
1. A basic docker compose.yaml has been provided.
2. Optionally, change any environmental variables other than `HEADLESS`, which must stay `=true`
2. Optionally, customize your config by adding any other environmental variables from the table below.
3. Build or rebuild and start the container using `docker compose up -d --build`
3. Build and start the container using `docker compose up -d`.
4. You can view logs with `docker logs netsky`
@@ -62,17 +63,17 @@ Under development, however mainly for personal use!
| 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 |
| 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_DO_DAILY_SET |
| workers.doMorePromotions | Complete promotional items | `true` | WORKERS_DO_MORE_PROMOTIONS |
| workers.doPunchCards | Complete punchcards | `true` | WORKERS_DO_PUNCH_CARDS |
| workers.doDesktopSearch | Complete daily desktop searches | `true` | WORKERS_DO_DESKTOP_SEARCH |
| workers.doMobileSearch | Complete daily mobile searches | `true` | WORKERS_DO_MOBILE_SEARCH |
| workers.doDailySet | Complete daily set items | `true` | DO_DAILY_SET |
| workers.doMorePromotions | Complete promotional items | `true` | DO_MORE_PROMOTIONS |
| workers.doPunchCards | Complete punchcards | `true` | DO_PUNCH_CARDS |
| workers.doDesktopSearch | Complete daily desktop searches | `true` | DO_DESKTOP_SEARCH |
| workers.doMobileSearch | Complete daily mobile searches | `true` | DO_MOBILE_SEARCH |
| globalTimeout | The length before the action gets timeout | `30000` (30 seconds) | GLOBAL_TIMEOUT |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | SEARCH_SETTINGS_USE_GEO_LOCALE_QUERIES |
| scrollRandomResults | Scroll randomly in search results | `true` | SEARCH_SETTINGS_SCROLL_RANDOM_RESULTS |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | SEARCH_SETTINGS_CLICK_RANDOM_RESULTS |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | USE_GEO_LOCALE_QUERIES |
| scrollRandomResults | Scroll randomly in search results | `true` | SCROLL_RANDOM_RESULTS |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | CLICK_RANDOM_RESULTS |
| 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 |
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | SEARCH_SETTINGS_RETRY_MOBILE_SEARCH |
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | RETRY_MOBILE_SEARCH |
| 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="" |
@@ -82,6 +83,7 @@ Under development, however mainly for personal use!
- [x] Multi-Account Support
- [x] Session Storing
- [x] 2FA Support
- [x] Passwordless Support
- [x] Headless Support
- [x] Discord Webhook Support
- [x] Desktop Searches
@@ -100,6 +102,8 @@ Under development, however mainly for personal use!
- [x] Completing Punchcards
- [x] Solving This Or That Quiz (Random)
- [x] Solving ABC Quiz
- [x] Completing Daily Check In
- [x] Completing Read To Earn
- [ ] Completing Shopping Game
- [ ] Completing Gaming Tab
- [x] Clustering Support

View File

@@ -6,29 +6,8 @@ services:
- TZ=America/Toronto #change to your local timezone
- NODE_ENV=production
- HEADLESS=true #do not change
### the following are optional, you only need to include them if you want to enter a custom value, removing them will use the default values
- BASE_URL=https://rewards.bing.com
- SESSION_PATH=sessions
- RUN_ON_ZERO_POINTS=false
- CLUSTERS=1
- SAVE_FINGERPRINT=false
- WORKERS_DO_DAILY_SET=true
- WORKERS_DO_MORE_PROMOTIONS=true
- WORKERS_DO_PUNCH_CARDS=true
- WORKERS_DO_DESKTOP_SEARCH=true
- WORKERS_DO_MOBILE_SEARCH=true
- SEARCH_SETTINGS_USE_GEO_LOCALE_QUERIES=false
- SEARCH_SETTINGS_SCROLL_RANDOM_RESULTS=true
- SEARCH_SETTINGS_CLICK_RANDOM_RESULTS=true
- SEARCH_SETTINGS_SEARCH_DELAY_MIN=10000 # Set the search delay longer, e.g. MIN=180000 and MAX=270000 if you live in a region where MS enforces a search cooldown
- SEARCH_SETTINGS_SEARCH_DELAY_MAX=20000
- SEARCH_SETTINGS_RETRY_MOBILE_SEARCH=true
- WEBHOOK_ENABLED=false
- WEBHOOK_URL=
### Customize your run schedule, default 5:00 am and 11:00 am, use crontab.guru if you're not sure
### 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 as false to only run the script per the cron schedule
### Run on start, set to false to only run the script per the cron schedule
- RUN_ON_START=true
restart: unless-stopped
volumes:
- .:/usr/src/microsoft-rewards-script
restart: unless-stopped

View File

@@ -1,6 +1,6 @@
{
"name": "microsoft-rewards-script",
"version": "1.4.7",
"version": "1.4.9",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
@@ -26,17 +26,17 @@
"author": "Netsky",
"license": "ISC",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.4.5"
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.7.2",
"cheerio": "^1.0.0-rc.12",
"fingerprint-generator": "^2.1.51",
"fingerprint-injector": "^2.1.51",
"playwright": "^1.44.1",
"axios": "^1.7.5",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.54",
"fingerprint-injector": "^2.1.54",
"playwright": "^1.46.1",
"ts-node": "^10.9.2"
}
}

View File

@@ -1,10 +1,12 @@
import { Page } from 'playwright'
import { CheerioAPI, load } from 'cheerio'
import axios, { AxiosRequestConfig } from 'axios'
import { MicrosoftRewardsBot } from '../index'
import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData'
import { QuizData } from './../interface/QuizData'
import { AppUserData } from '../interface/AppUserData'
export default class BrowserFunc {
@@ -131,10 +133,10 @@ export default class BrowserFunc {
}
/**
* Get total earnable points
* Get total earnable points with web browser
* @returns {number} Total earnable points
*/
async getEarnablePoints(): Promise<number> {
async getBrowserEarnablePoints(): Promise<number> {
try {
const data = await this.getDashboardData()
@@ -158,7 +160,7 @@ export default class BrowserFunc {
if (data.morePromotions?.length) {
data.morePromotions.forEach(x => {
// Only count points from supported activities
if (['quiz', 'urlreward'].includes(x.promotionType)) {
if (['quiz', 'urlreward'].includes(x.promotionType) && !x.attributes.is_unlocked) {
totalEarnablePoints += (x.pointProgressMax - x.pointProgress)
}
})
@@ -166,7 +168,57 @@ export default class BrowserFunc {
return totalEarnablePoints
} 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

@@ -10,7 +10,9 @@
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true
},
"globalTimeout": 30000,
"searchSettings": {

View File

@@ -1 +1 @@
${CRON_START_TIME} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /var/log/cron.log 2>&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 { ThisOrThat } from './activities/ThisOrThat'
import { UrlReward } from './activities/UrlReward'
import { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData } from '../interface/DashboardData'
@@ -49,4 +51,14 @@ export default class Activities {
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

@@ -3,6 +3,9 @@ import readline from 'readline'
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({
input: process.stdin,
@@ -12,6 +15,11 @@ const rl = readline.createInterface({
export class Login {
private bot: MicrosoftRewardsBot
private clientId: string = '0000000040170455'
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
@@ -56,51 +64,110 @@ export class Login {
private async execLogin(page: Page, email: string, password: string) {
try {
// Enter email
await page.fill('#i0116', email)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully')
try {
// Enter password
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
await this.bot.utils.wait(2000)
await page.fill('#i0118', password)
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.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
}
this.bot.log('LOGIN', 'Password entered successfully')
} catch (error) {
this.bot.log('LOGIN', 'An error occurred:' + error, 'error')
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')
}
}
const currentURL = new URL(page.url())
private async enterEmail(page: Page, email: string) {
await page.fill('#i0116', email)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully')
}
while (currentURL.pathname !== '/' || currentURL.hostname !== 'rewards.bing.com') {
private async enterPassword(page: Page, password: string) {
try {
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
await this.bot.utils.wait(2000)
await page.fill('#i0118', password)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Password entered successfully')
} catch {
this.bot.log('LOGIN', 'Password entry failed or 2FA required')
await this.handle2FA(page)
}
}
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}`)
}
}
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)
currentURL.href = page.url()
const currentURL = new URL(page.url())
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
break
}
}
// Wait for login to complete
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> {
@@ -141,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

@@ -58,7 +58,7 @@ export class Workers {
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
// Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { })
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
await this.solveActivities(page, activitiesUncompleted, punchCard)
@@ -87,7 +87,7 @@ export class Workers {
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) {
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) {
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} 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
@@ -149,7 +149,7 @@ export class Workers {
// 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.
*/
await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { })

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

@@ -42,6 +42,8 @@ export class Search extends Workers {
// 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) })
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
// Loop over Google search queries
for (let i = 0; i < queries.length; i++) {
const query = queries[i] as string
@@ -98,7 +100,7 @@ export class Search extends Workers {
for (const term of relatedTerms.slice(1, 3)) {
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)
// If the new point amount is the same as before
@@ -131,15 +133,19 @@ export class Search extends Workers {
private async bingSearch(searchPage: Page, query: string) {
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
// Try a max of 5 times
for (let i = 0; i < 5; i++) {
try {
// 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'
@@ -153,14 +159,19 @@ export class Search extends Workers {
await searchPage.keyboard.type(query)
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) {
await this.bot.utils.wait(2000)
await this.randomScroll(searchPage)
await this.randomScroll(resultPage)
}
if (this.bot.config.searchSettings.clickRandomResults) {
await this.bot.utils.wait(2000)
await this.clickRandomLink(searchPage)
await this.clickRandomLink(resultPage)
}
// Delay between searches
@@ -179,7 +190,7 @@ export class Search extends Workers {
// Reset the tabs
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)
}
@@ -260,11 +271,13 @@ export class Search extends Workers {
private async randomScroll(page: Page) {
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) => {
window.scrollBy(0, scrollAmount)
}, scrollAmount)
await page.evaluate((scrollPos) => {
window.scrollTo(0, scrollPos)
}, randomScrollPosition)
} catch (error) {
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
@@ -273,23 +286,22 @@ export class Search extends Workers {
private async clickRandomLink(page: Page) {
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
let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Let website load, if it doesn't load within 5 sec. exit regardless
await this.bot.utils.wait(5000)
// Stay for 10 seconds
await this.bot.utils.wait(10_000)
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.
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
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
@@ -302,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 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) {
await lastTab.close()
// If only 1 tab is open, open a new one to search in
} else if (tabs.length === 1) {
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 {
await lastTab.goBack()
await lastTab.goBack().catch(() => { })
}
}
private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches
@@ -334,4 +345,4 @@ export class Search extends Workers {
return missingPoints
}
}
}

View File

@@ -34,6 +34,7 @@ export class MicrosoftRewardsBot {
private accounts: Account[]
private workers: Workers
private login = new Login(this)
private accessToken: string = ''
constructor() {
this.log = log
@@ -134,14 +135,19 @@ export class MicrosoftRewardsBot {
// Login into MS Rewards, then go to rewards homepage
await this.login.login(this.homePage, account.email, account.password)
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
await this.browser.func.goHome(this.homePage)
const data = await this.browser.func.getDashboardData()
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
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 (!this.config.runOnZeroPoints && this.collectedPoints === 0) {
@@ -200,42 +206,49 @@ export class MicrosoftRewardsBot {
const data = await this.browser.func.getDashboardData()
// If no mobile searches data found, stop (Does not exist on new accounts)
if (!data.userStatus.counters.mobileSearch) {
log('MAIN', 'No mobile searches found, stopping!')
// Close mobile browser
return await this.closeBrowser(browser, account.email)
// Do daily check in
if (this.config.workers.doDailyCheckIn) {
await this.activities.doDailyCheckIn(this.accessToken, data)
}
// Open a new tab to where the tasks are going to be completed
const workerPage = await browser.newPage()
// Do read to earn
if (this.config.workers.doReadToEarn) {
await this.activities.doReadToEarn(this.accessToken, data)
}
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// If no mobile searches data found, stop (Does not 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()
// Do mobile searches
if (this.config.workers.doMobileSearch) {
await this.activities.doSearch(workerPage, data)
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// Fetch current search points
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
// Do mobile searches
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
// 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...')
// Fetch current search points
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
// Close mobile browser
await this.closeBrowser(browser, account.email)
// If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid
// 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
await this.Mobile(account)
// Close mobile browser
await this.closeBrowser(browser, account.email)
// Retry
await this.Mobile(account)
}
}
} else {
log('MAIN', 'No mobile searches found!')
}
// 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
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)

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

@@ -35,4 +35,6 @@ export interface Workers {
doPunchCards: boolean;
doDesktopSearch: 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;
}

View File

@@ -3,6 +3,9 @@
# 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

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