6 Commits

Author SHA1 Message Date
TheNetsky
0ddc964878 1.5.2
- Updated packages
- Cleanup
- Improved message dismissal
- Improved login functionality

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

* Update README.md
2025-03-03 10:59:06 +01:00
HMCDAT
6f19bd4b0e Update Dockerfile (#262) 2025-02-28 12:34:47 +01:00
AariaX
caf6a42a38 Make shuffleArray more random? (#254) 2025-02-25 16:14:51 +01:00
TheNetsky
352d47229b Merge branch 'main' of https://github.com/TheNetsky/Microsoft-Rewards-Script 2025-02-24 17:59:28 +01:00
TheNetsky
9a12ee1ec8 Fix geoLocale not being uppercase 2025-02-24 17:59:20 +01:00
14 changed files with 105 additions and 51 deletions

View File

@@ -37,6 +37,7 @@ RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally
CMD ["sh", "-c", "echo \"$TZ\" > /etc/timezone && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && \
chmod 0644 /etc/cron.d/microsoft-rewards-cron && \

View File

@@ -57,7 +57,7 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| baseURL | MS Rewards page | `https://rewards.bing.com` |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
| parallel | If you want mobile and desktop taks to run parallel or sequential| `true` |
| parallel | If you want mobile and desktop tasks to run parallel or sequential| `true` |
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) |
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
| saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
@@ -74,8 +74,8 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
| searchSettings.scrollRandomResults | Scroll randomly in search results | `true` |
| searchSettings.clickRandomResults | Visit random website from search result| `true` |
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 1min` `max: 2min` |
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `3` |
| searchSettings.searchDelay | Minimum and maximum time in milliseconds between search queries | `min: 3min` `max: 5min` |
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `2` |
| logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` |
| webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` |
| proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) |

View File

@@ -1,6 +1,6 @@
{
"name": "microsoft-rewards-script",
"version": "1.5.1",
"version": "1.5.2",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
@@ -35,11 +35,11 @@
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.7.9",
"axios": "^1.8.4",
"chalk": "^4.1.2",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.62",
"fingerprint-injector": "^2.1.62",
"fingerprint-generator": "^2.1.66",
"fingerprint-injector": "^2.1.66",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"ms": "^2.1.3",

View File

@@ -46,7 +46,7 @@ class Browser {
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
// Set timeout to preferred amount
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30_000))
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
await context.addCookies(sessionData.cookies)

View File

@@ -298,7 +298,7 @@ export default class BrowserFunc {
async waitForQuizRefresh(page: Page): Promise<boolean> {
try {
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 })
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000)
return true

View File

@@ -13,6 +13,7 @@ export default class BrowserUtil {
async tryDismissAllMessages(page: Page): Promise<boolean> {
const buttons = [
{ selector: 'button[type="submit"]', label: 'Submit Button' },
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
@@ -23,21 +24,25 @@ export default class BrowserUtil {
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' },
{ selector: '.maybe-later', label: 'Mobile Rewards App Banner' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container', isXPath: true },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
]
const dismissTasks = buttons.map(async (button) => {
try {
const element = page.locator(button.selector)
const element = button.isXPath
? page.locator(`xpath=${button.selector}`)
: page.locator(button.selector)
if (await element.first().isVisible({ timeout: 1000 })) {
await element.first().click({ timeout: 1000 })
await page.waitForTimeout(500)
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
return true
}
} catch (error) {
// Ignore errors and continue
} catch {
// Silent fail
}
return false
})

View File

@@ -39,7 +39,7 @@ export class Login {
// Check if account is locked
await this.checkAccountLocked(page)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
if (!isLoggedIn) {
await this.execLogin(page, email, password)
@@ -86,30 +86,80 @@ export class Login {
}
private async enterEmail(page: Page, email: string) {
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 2_000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
return
}
const emailInputSelector = 'input[type="email"]'
await page.fill('#i0116', email)
await page.click('#idSIButton9')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
try {
// Wait for email field
const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).catch(() => null)
if (!emailField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
return
}
await this.bot.utils.wait(1000)
// Check if email is prefilled
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 5000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
} else {
// Else clear and fill email
await page.fill(emailInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(emailInputSelector, email)
await this.bot.utils.wait(1000)
}
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
}
}
private async enterPassword(page: Page, password: string) {
const passwordInputSelector = 'input[type="password"]'
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(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} catch {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entry failed or 2FA required')
// Wait for password field
const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
if (!passwordField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
await this.handle2FA(page)
return
}
await this.bot.utils.wait(1000)
// Clear and fill password
await page.fill(passwordInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(passwordInputSelector, password)
await this.bot.utils.wait(1000)
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
await this.handle2FA(page)
}
}
private async handle2FA(page: Page) {
try {
const numberToPress = await this.get2FACode(page)
@@ -138,7 +188,7 @@ export class Login {
while (true) {
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
if (button) {
await this.bot.utils.wait(60_000)
await this.bot.utils.wait(60000)
await button.click()
continue
@@ -148,7 +198,7 @@ export class Login {
}
}
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => {})
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent()
@@ -162,7 +212,7 @@ export class Login {
this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60_000 })
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
break
@@ -203,7 +253,7 @@ export class Login {
}
// Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 })
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal')
}
@@ -272,7 +322,7 @@ export class Login {
currentUrl = new URL(page.url())
await this.bot.utils.wait(5000)
}
const body = new URLSearchParams()
body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId)

View File

@@ -153,7 +153,7 @@ export class Workers {
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(() => { })
await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(2000)
switch (activity.promotionType) {

View File

@@ -15,18 +15,18 @@ export class ABC extends Workers {
const maxIterations = 15
let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 })
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10000 })
const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 })
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000)
await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 })
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10000 })
await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -11,7 +11,7 @@ export class Poll extends Workers {
try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10000 }).catch(() => { }) // We're gonna click regardless or not
await this.bot.utils.wait(2000)
await page.click(buttonId)

View File

@@ -29,7 +29,7 @@ export class Quiz extends Workers {
const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
@@ -59,7 +59,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
if (dataOption === correctOption) {

View File

@@ -47,7 +47,7 @@ export class Search extends Workers {
await this.bot.browser.utils.tryDismissAllMessages(page)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it doesn't continue after 5 more searches with alternative queries, abort search
const queries: string[] = []
// Mobile search doesn't seem to like related queries?
@@ -156,7 +156,7 @@ export class Search extends Workers {
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 })
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500)
await searchPage.keyboard.down(platformControlKey)
@@ -222,7 +222,7 @@ export class Search extends Workers {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale}\\", 0, null, 48]"]]]`
data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale.toUpperCase()}\\", 0, null, 48]"]]]`
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
@@ -312,7 +312,7 @@ export class Search extends Workers {
await this.closeContinuePopup(page)
// Stay for 10 seconds for page to load and "visit"
await this.bot.utils.wait(10_000)
await this.bot.utils.wait(10000)
// Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
let lastTab = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -20,7 +20,7 @@ export class SearchOnBing extends Workers {
const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q'
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 })
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await page.click(searchBar)
await this.bot.utils.wait(500)
await page.keyboard.type(query)

View File

@@ -18,11 +18,9 @@ export default class Util {
}
shuffleArray<T>(array: T[]): T[] {
const shuffledArray = array.slice()
shuffledArray.sort(() => Math.random() - 0.5)
return shuffledArray
return array.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
}
randomNumber(min: number, max: number): number {