7 Commits

Author SHA1 Message Date
Netsky
f015649d16 Fix typo 2024-10-02 18:42:01 +02:00
Netsky
034359019c Update README.md 2024-10-02 18:41:06 +02:00
HMCDAT
09ddbee45a 1.4.11 (#146)
* fix misspelling

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

* remove unnecessary output

* Fix Formatting

* replace checking variable

* decrease id length

* revert previous commit

* change condition for eligible promo

* Other fixes

---------

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

* update readme

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

* fix some stuff

* make ReadToEarn use the delay config

* fix daily reward search per week

* reorder mobile tasks

* fix message

* Search fixes, reformatting and types

---------

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

View File

@@ -24,17 +24,23 @@ RUN apt-get install -y \
# Install application dependencies
RUN npm install
# Ensure correct permissions for node_modules
RUN chmod -R 755 /usr/src/microsoft-rewards-script/node_modules
# Install Playwright Chromium directly from local node_modules
RUN ./node_modules/.bin/playwright install chromium
# Build the script
RUN npm run build
# Install playwright chromium
RUN npx playwright install chromium
# Copy cron file to cron directory
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
# Ensure correct permissions for the working directory
RUN chmod -R 755 /usr/src/microsoft-rewards-script
# 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

@@ -68,8 +68,10 @@ Under development, however mainly for personal use!
| 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 |
| workers.doDailyCheckIn | Complete daily check-in activity | `true` | DO_DAILY_CHECK_IN |
| workers.doReadToEarn | Complete read to earn activity | `true` | DO_READ_TO_EARN |
| 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) | USE_GEO_LOCALE_QUERIES |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `true` (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 |
@@ -102,6 +104,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

@@ -1,6 +1,6 @@
{
"name": "microsoft-rewards-script",
"version": "1.4.8",
"version": "1.4.10",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
@@ -32,11 +32,11 @@
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.7.4",
"axios": "^1.7.5",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.54",
"fingerprint-injector": "^2.1.54",
"playwright": "^1.46.1",
"fingerprint-generator": "^2.1.56",
"fingerprint-injector": "^2.1.56",
"playwright": "^1.47.2",
"ts-node": "^10.9.2"
}
}

View File

@@ -13,6 +13,7 @@ import { AccountProxy } from '../interface/Account'
https://abrahamjuliot.github.io/creepjs/
https://botcheck.luminati.io/
http://f.vision/
https://pixelscan.net/
*/
class Browser {
@@ -61,7 +62,7 @@ class Browser {
const fingerPrintData = new FingerprintGenerator().getFingerprint({
devices: this.bot.isMobile ? ['mobile'] : ['desktop'],
operatingSystems: this.bot.isMobile ? ['android'] : ['windows'],
browsers: ['edge']
browserListQuery: 'last 2 edge version'
})
return fingerPrintData

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) && !x.attributes.is_unlocked) {
if (['quiz', 'urlreward'].includes(x.promotionType) && x.exclusiveLockedFeatureStatus !== 'locked') {
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,11 +10,13 @@
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true
},
"globalTimeout": 30000,
"searchSettings": {
"useGeoLocaleQueries": false,
"useGeoLocaleQueries": true,
"scrollRandomResults": true,
"clickRandomResults": true,
"searchDelay": {

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
@@ -200,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 && !x.attributes.is_unlocked) ?? []
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && x.exclusiveLockedFeatureStatus !== 'locked') ?? []
if (!activitiesUncompleted.length) {
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')

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 userBalance = userData.balance
const jsonData = {
amount: 1,
country: geoLocale,
id: '1',
type: 101,
attributes: {
offerid: 'ENUS_readarticle3_30points'
}
}
const articleCount = 10
for (let i = 0; i < articleCount; ++i) {
jsonData.id = randomBytes(64).toString('hex')
const claimRequest = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
},
data: JSON.stringify(jsonData)
}
const claimResponse = await axios(claimRequest)
const newBalance = (await claimResponse.data).response.balance
if (newBalance == userBalance) {
this.bot.log('READ-TO-EARN', 'Read all available articles')
break
} else {
this.bot.log('READ-TO-EARN', `Read article ${i + 1} of ${articleCount} max | Gained ${newBalance - userBalance} Points`)
userBalance = newBalance
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.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

@@ -10,11 +10,11 @@ import { GoogleSearch } from '../../interface/Search'
export class Search extends Workers {
private searchPageURL = 'https://bing.com'
private bingHome = 'https://bing.com'
private searchPageURL = ''
public async doSearch(page: Page, data: DashboardData) {
this.bot.log('SEARCH-BING', 'Starting bing searches')
this.bot.log('SEARCH-BING', 'Starting Bing searches')
page = await this.bot.browser.utils.getLatestTab(page)
@@ -34,7 +34,7 @@ export class Search extends Workers {
googleSearchQueries = [...new Set(googleSearchQueries)]
// Go to bing
await page.goto(this.searchPageURL)
await page.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search
@@ -136,6 +136,8 @@ export class Search extends Workers {
// Try a max of 5 times
for (let i = 0; i < 5; i++) {
try {
// This page had already been set to the Bing.com page or the previous search listing, we just need to select it
searchPage = await this.bot.browser.utils.getLatestTab(searchPage)
// Go to top of the page
await searchPage.evaluate(() => {
@@ -155,14 +157,20 @@ 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)
this.searchPageURL = new URL(resultPage.url()).href // Set the results page
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
@@ -176,12 +184,13 @@ export class Search extends Workers {
break
}
this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
// 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)
}
@@ -247,7 +256,7 @@ export class Search extends Workers {
return response.data[1] as string[]
} catch (error) {
this.bot.log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error')
this.bot.log('SEARCH-BING-RELATED', 'An error occurred:' + error, 'error')
}
return []
}
@@ -262,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')
@@ -275,23 +286,21 @@ 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', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not
// Stay for 10 seconds for page to load and "visit"
await this.bot.utils.wait(10_000)
// Will get current tab if no new one is created
// Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Let website load, if it doesn't load within 5 sec. exit regardless
await this.bot.utils.wait(5000)
let lastTabURL = new URL(lastTab.url()) // Get new tab info
let lastTabURL = new URL(lastTab.url()) // Get new tab info, this is the website we're visiting
// Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0
while (lastTabURL.href !== 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
@@ -304,26 +313,29 @@ 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 this.bot.utils.wait(1000)
await newPage.goto(this.bingHome)
await this.bot.utils.wait(3000)
this.searchPageURL = newPage.url()
// Else go back one page
// Else reset the last tab back to the search listing or Bing.com
} else {
await lastTab.goBack()
lastTab = await this.bot.browser.utils.getLatestTab(lastTab)
await lastTab.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
}
}
private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches

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

View File

@@ -351,6 +351,8 @@ export interface MorePromotion {
legalText: string;
legalLinkText: string;
deviceType: string;
exclusiveLockedFeatureType: string;
exclusiveLockedFeatureStatus: string;
}
export interface PunchCard {

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

@@ -1,41 +1,43 @@
const fs = require('fs');
const path = require('path');
const fs = require('fs')
const path = require('path')
const configPath = path.join(__dirname, '../dist/config.json');
const configPath = path.join(__dirname, '../dist/config.json')
// Read the existing config file
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
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.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.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.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;
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');
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);
console.error(`Failed to write updated config file to ${configPath}:`, error)
process.exit(1)
}