13 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
Netsky
b630c3ddda Update README.md 2025-02-24 15:52:53 +01:00
Netsky
287e3897da Update README.md 2025-02-24 15:51:08 +01:00
Netsky
fcf6aba446 Bump version 2025-02-24 15:49:45 +01:00
AariaX
1102f2ca94 Google API Update (1.5.1) (#247)
* switch to google internal api

* A bit of tidying up

* A bit more of tidying up

* A bit more of tidying up

* Pre 1.5.1

- Add proxy exclusions
- Update ReadMe
- Update config Interface

---------

Co-authored-by: TheNetsky <56271887+TheNetsky@users.noreply.github.com>
2025-02-24 15:49:19 +01:00
1OSA
82a896e83f added webhooklogexcludefunc (#240) 2025-02-20 23:21:46 +01:00
Netsky
b0bd1f52c4 Update queries.json 2025-02-19 19:32:45 +01:00
Netsky
7e4121e01b Update run_daily.sh 2025-02-19 15:29:51 +01:00
20 changed files with 359 additions and 117 deletions

View File

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

View File

@@ -57,9 +57,11 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| baseURL | MS Rewards page | `https://rewards.bing.com` | | baseURL | MS Rewards page | `https://rewards.bing.com` |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) | | sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) | | headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
| 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) | | 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) | | clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
| saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | | saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
| saveFingerprint.desktop | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
| workers.doDailySet | Complete daily set items | `true` | | workers.doDailySet | Complete daily set items | `true` |
| workers.doMorePromotions | Complete promotional items | `true` | | workers.doMorePromotions | Complete promotional items | `true` |
| workers.doPunchCards | Complete punchcards | `true` | | workers.doPunchCards | Complete punchcards | `true` |
@@ -67,17 +69,19 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| workers.doMobileSearch | Complete daily mobile searches | `true` | | workers.doMobileSearch | Complete daily mobile searches | `true` |
| workers.doDailyCheckIn | Complete daily check-in activity | `true` | | workers.doDailyCheckIn | Complete daily check-in activity | `true` |
| workers.doReadToEarn | Complete read to earn activity | `true` | | workers.doReadToEarn | Complete read to earn activity | `true` |
| searchOnBingLocalQueries | Complete the activity "search on Bing" using the `queries.json` or fetched from this repo | `false` (Will fetch from this repo) |
| globalTimeout | The length before the action gets timeout | `30s` | | globalTimeout | The length before the action gets timeout | `30s` |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `true` (Uses EN-US generated queries) | | searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
| scrollRandomResults | Scroll randomly in search results | `true` | | searchSettings.scrollRandomResults | Scroll randomly in search results | `true` |
| searchSettings.clickRandomResults | Visit random website from search result| `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.searchDelay | Minimum and maximum time in milliseconds between search queries | `min: 3min` `max: 5min` |
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `3` | | searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `2` |
| logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` | | logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` |
| webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` |
| proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
| proxy.proxyBingTerms | Enable or disable proxying the request via set proxy | `true` (will be proxied) |
| webhook.enabled | Enable or disable your set webhook | `false` | | webhook.enabled | Enable or disable your set webhook | `false` |
| webhook.url | Your Discord webhook URL | `null` | | webhook.url | Your Discord webhook URL | `null` |
| cronStartTime | Scheduled script run-time, *only available for docker implementation* | `0 5,11 * * *` (5:00 am, 11:00 am daily) |
| | Run the script immediately when the Docker container starts | `true` |
## Features ## ## Features ##
- [x] Multi-Account Support - [x] Multi-Account Support
@@ -112,4 +116,4 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
## Disclaimer ## ## Disclaimer ##
Your account may be at risk of getting banned or suspended using this script, you've been warned! Your account may be at risk of getting banned or suspended using this script, you've been warned!
<br /> <br />
Use this script at your own risk! Use this script at your own risk!

View File

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

View File

@@ -46,7 +46,7 @@ class Browser {
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint }) const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
// Set timeout to preferred amount // 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) await context.addCookies(sessionData.cookies)

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"baseURL": "https://rewards.bing.com", "baseURL": "https://rewards.bing.com",
"sessionPath": "sessions", "sessionPath": "sessions",
"headless": true, "headless": false,
"parallel": true, "parallel": true,
"runOnZeroPoints": false, "runOnZeroPoints": false,
"clusters": 1, "clusters": 1,
@@ -33,6 +33,13 @@
"logExcludeFunc": [ "logExcludeFunc": [
"SEARCH-CLOSE-TABS" "SEARCH-CLOSE-TABS"
], ],
"webhookLogExcludeFunc": [
"SEARCH-CLOSE-TABS"
],
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"webhook": { "webhook": {
"enabled": false, "enabled": false,
"url": "" "url": ""

View File

@@ -39,7 +39,7 @@ export class Login {
// Check if account is locked // Check if account is locked
await this.checkAccountLocked(page) 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) { if (!isLoggedIn) {
await this.execLogin(page, email, password) await this.execLogin(page, email, password)
@@ -86,30 +86,80 @@ export class Login {
} }
private async enterEmail(page: Page, email: string) { private async enterEmail(page: Page, email: string) {
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 2_000 }).catch(() => null) const emailInputSelector = 'input[type="email"]'
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
return
}
await page.fill('#i0116', email) try {
await page.click('#idSIButton9') // Wait for email field
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully') const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).catch(() => null)
if (!emailField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
return
}
await this.bot.utils.wait(1000)
// Check if email is prefilled
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 5000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
} else {
// Else clear and fill email
await page.fill(emailInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(emailInputSelector, email)
await this.bot.utils.wait(1000)
}
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
}
} }
private async enterPassword(page: Page, password: string) { private async enterPassword(page: Page, password: string) {
const passwordInputSelector = 'input[type="password"]'
try { try {
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) // Wait for password field
await this.bot.utils.wait(2000) const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
await page.fill('#i0118', password) if (!passwordField) {
await page.click('#idSIButton9') this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully') await this.handle2FA(page)
} catch { return
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entry failed or 2FA required') }
await this.bot.utils.wait(1000)
// Clear and fill password
await page.fill(passwordInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(passwordInputSelector, password)
await this.bot.utils.wait(1000)
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
await this.handle2FA(page) await this.handle2FA(page)
} }
} }
private async handle2FA(page: Page) { private async handle2FA(page: Page) {
try { try {
const numberToPress = await this.get2FACode(page) const numberToPress = await this.get2FACode(page)
@@ -138,7 +188,7 @@ export class Login {
while (true) { while (true) {
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null) const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
if (button) { if (button) {
await this.bot.utils.wait(60_000) await this.bot.utils.wait(60000)
await button.click() await button.click()
continue 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) await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent() return await element.textContent()
@@ -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', `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') 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!') this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
break break
@@ -203,7 +253,7 @@ export class Login {
} }
// Wait for login to complete // Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }) await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal') 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()) currentUrl = new URL(page.url())
await this.bot.utils.wait(5000) await this.bot.utils.wait(5000)
} }
const body = new URLSearchParams() const body = new URLSearchParams()
body.append('grant_type', 'authorization_code') body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId) 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, Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load. if it didn't then it gave enough time for the page to load.
*/ */
await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { }) await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
switch (activity.promotionType) { switch (activity.promotionType) {

View File

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

View File

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

View File

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

View File

@@ -4,9 +4,17 @@ import { platform } from 'os'
import { Workers } from '../Workers' import { Workers } from '../Workers'
import { Counters, DashboardData } from '../../interface/DashboardData' import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleTrends } from '../../interface/GoogleDailyTrends'
import { GoogleSearch } from '../../interface/Search' import { GoogleSearch } from '../../interface/Search'
import { AxiosRequestConfig } from 'axios'
type GoogleTrendsResponse = [
string,
[
string,
...null[],
[string, ...string[]]
][]
];
export class Search extends Workers { export class Search extends Workers {
private bingHome = 'https://bing.com' private bingHome = 'https://bing.com'
@@ -26,7 +34,7 @@ export class Search extends Workers {
} }
// Generate search queries // Generate search queries
let googleSearchQueries = await this.getGoogleTrends(data.userProfile.attributes.country, missingPoints) let googleSearchQueries = await this.getGoogleTrends(this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US')
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries) googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Deduplicate the search terms // Deduplicate the search terms
@@ -39,7 +47,7 @@ export class Search extends Workers {
await this.bot.browser.utils.tryDismissAllMessages(page) 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[] = [] const queries: string[] = []
// Mobile search doesn't seem to like related queries? // Mobile search doesn't seem to like related queries?
@@ -148,7 +156,7 @@ export class Search extends Workers {
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
const searchBar = '#sb_form_q' const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 }) await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await searchPage.click(searchBar) // Focus on the textarea await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
await searchPage.keyboard.down(platformControlKey) await searchPage.keyboard.down(platformControlKey)
@@ -203,49 +211,64 @@ export class Search extends Workers {
return await this.bot.browser.func.getSearchPoints() return await this.bot.browser.func.getSearchPoints()
} }
private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> { private async getGoogleTrends(geoLocale: string = 'US'): Promise<GoogleSearch[]> {
const queryTerms: GoogleSearch[] = [] const queryTerms: GoogleSearch[] = []
let i = 0
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US'
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`) this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
while (queryCount > queryTerms.length) { try {
i += 1 const request: AxiosRequestConfig = {
const date = new Date() url: 'https://trends.google.com/_/TrendsUi/data/batchexecute',
date.setDate(date.getDate() - i) method: 'POST',
const formattedDate = this.formatDate(date) headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
try { },
const request = { data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale.toUpperCase()}\\", 0, null, 48]"]]]`
url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
const response = await this.bot.axios.request(request)
const data: GoogleTrends = JSON.parse((await response.data).slice(5))
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
queryTerms.push({
topic: topic.title.query.toLowerCase(),
related: topic.relatedQueries.map(x => x.query.toLowerCase())
})
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
break
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
const rawText = response.data
const trendsData = this.extractJsonFromResponse(rawText)
if (!trendsData) {
throw this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
}
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
if (mappedTrendsData.length < 90) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Insufficient search queries, falling back to US', 'warn')
return this.getGoogleTrends()
}
for (const [topic, relatedQueries] of mappedTrendsData) {
queryTerms.push({
topic: topic as string,
related: relatedQueries as string[]
})
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
} }
return queryTerms return queryTerms
} }
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
const lines = text.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch {
continue
}
}
}
return null
}
private async getRelatedTerms(term: string): Promise<string[]> { private async getRelatedTerms(term: string): Promise<string[]> {
try { try {
const request = { const request = {
@@ -256,7 +279,7 @@ export class Search extends Workers {
} }
} }
const response = await this.bot.axios.request(request) const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyBingTerms)
return response.data[1] as string[] return response.data[1] as string[]
} catch (error) { } catch (error) {
@@ -266,14 +289,6 @@ export class Search extends Workers {
return [] return []
} }
private formatDate(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}${month}${day}`
}
private async randomScroll(page: Page) { private async randomScroll(page: Page) {
try { try {
const viewportHeight = await page.evaluate(() => window.innerHeight) const viewportHeight = await page.evaluate(() => window.innerHeight)
@@ -297,7 +312,7 @@ export class Search extends Workers {
await this.closeContinuePopup(page) await this.closeContinuePopup(page)
// Stay for 10 seconds for page to load and "visit" // 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 // Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
let lastTab = await this.bot.browser.utils.getLatestTab(page) let lastTab = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -20,7 +20,7 @@ export class SearchOnBing extends Workers {
const query = await this.getSearchQuery(activity.title) const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q' 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 page.click(searchBar)
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
await page.keyboard.type(query) await page.keyboard.type(query)

View File

@@ -134,5 +134,156 @@
"Japan time", "Japan time",
"New York time" "New York time"
] ]
},
{
"title": "Maisons près de chez vous",
"queries": [
"Maisons près de chez moi"
]
},
{
"title": "Vous ressentez des symptômes ?",
"queries": [
"Éruption cutanée sur l'avant-bras",
"Nez bouché",
"Toux chatouilleuse"
]
},
{
"title": "Faites vos achats plus vite",
"queries": [
"Acheter une PS5",
"Acheter une Xbox",
"Offres sur les chaises"
]
},
{
"title": "Traduisez tout !",
"queries": [
"Traduction bienvenue à la maison en coréen",
"Traduction bienvenue à la maison en japonais",
"Traduction au revoir en japonais"
]
},
{
"title": "Rechercher paroles de chanson",
"queries": [
"Paroles de Debarge rhythm of the night"
]
},
{
"title": "Et si nous regardions ce film une nouvelle fois?",
"queries": [
"Alien film",
"Film Aliens",
"Film Alien 3",
"Film Predator"
]
},
{
"title": "Planifiez une petite escapade",
"queries": [
"Vols Amsterdam-Tokyo",
"Vols New York-Tokyo"
]
},
{
"title": "Consulter postes à pourvoir",
"queries": [
"emplois chez Microsoft",
"Offres d'emploi Microsoft",
"Emplois près de chez moi",
"emplois chez Boeing"
]
},
{
"title": "Vous pouvez suivre votre colis",
"queries": [
"Suivi Chronopost"
]
},
{
"title": "Trouver un endroit à découvrir",
"queries": [
"Itinéraire vers Berlin",
"Itinéraire vers Tokyo",
"Itinéraire vers New York"
]
},
{
"title": "Trop fatigué pour cuisiner ce soir ?",
"queries": [
"KFC près de chez moi",
"Burger King près de chez moi",
"McDonalds près de chez moi"
]
},
{
"title": "Convertissez rapidement votre argent",
"queries": [
"convertir 250 EUR en yen",
"convertir 500 EUR en yen"
]
},
{
"title": "Apprenez à cuisiner une nouvelle recette",
"queries": [
"Comment faire cuire la ratatouille",
"Comment faire cuire les lasagnes"
]
},
{
"title": "Trouvez des emplacements pour rester!",
"queries": [
"Hôtels Berlin Allemagne",
"Hôtels Amsterdam Pays-Bas"
]
},
{
"title": "Comment se porte l'économie ?",
"queries": [
"CAC 40"
]
},
{
"title": "Qui a gagné ?",
"queries": [
"score du Paris Saint-Germain"
]
},
{
"title": "Temps de jeu",
"queries": [
"Jeu vidéo Overwatch",
"Jeu vidéo Call of Duty"
]
},
{
"title": "Enrichissez votre vocabulaire",
"queries": [
"definition definition"
]
},
{
"title": "Quelle heure est-il ?",
"queries": [
"Heure du Japon",
"Heure de New York"
]
},
{
"title": "Vérifier la météo",
"queries": [
"Météo de Paris",
"Météo de la France"
]
},
{
"title": "Tenez-vous informé des sujets d'actualité",
"queries": [
"Augmentation Impots",
"Mort célébrité"
]
} }
] ]

View File

@@ -5,13 +5,15 @@ export interface Config {
parallel: boolean; parallel: boolean;
runOnZeroPoints: boolean; runOnZeroPoints: boolean;
clusters: number; clusters: number;
workers: Workers; saveFingerprint: ConfigSaveFingerprint;
workers: ConfigWorkers;
searchOnBingLocalQueries: boolean; searchOnBingLocalQueries: boolean;
globalTimeout: number | string; globalTimeout: number | string;
searchSettings: SearchSettings; searchSettings: ConfigSearchSettings;
webhook: Webhook;
logExcludeFunc: string[]; logExcludeFunc: string[];
saveFingerprint: ConfigSaveFingerprint; webhookLogExcludeFunc: string[];
proxy: ConfigProxy;
webhook: ConfigWebhook;
} }
export interface ConfigSaveFingerprint { export interface ConfigSaveFingerprint {
@@ -19,25 +21,30 @@ export interface ConfigSaveFingerprint {
desktop: boolean; desktop: boolean;
} }
export interface SearchSettings { export interface ConfigSearchSettings {
useGeoLocaleQueries: boolean; useGeoLocaleQueries: boolean;
scrollRandomResults: boolean; scrollRandomResults: boolean;
clickRandomResults: boolean; clickRandomResults: boolean;
searchDelay: SearchDelay; searchDelay: ConfigSearchDelay;
retryMobileSearchAmount: number; retryMobileSearchAmount: number;
} }
export interface SearchDelay { export interface ConfigSearchDelay {
min: number | string; min: number | string;
max: number | string; max: number | string;
} }
export interface Webhook { export interface ConfigWebhook {
enabled: boolean; enabled: boolean;
url: string; url: string;
} }
export interface Workers { export interface ConfigProxy {
proxyGoogleTrends: boolean;
proxyBingTerms: boolean;
}
export interface ConfigWorkers {
doDailySet: boolean; doDailySet: boolean;
doMorePromotions: boolean; doMorePromotions: boolean;
doPunchCards: boolean; doPunchCards: boolean;

View File

@@ -28,8 +28,5 @@ sleep $SLEEPTIME
# Log the start of the script # Log the start of the script
echo "Starting script..." echo "Starting script..."
# Update config with environment variables before running the script
node src/updateConfig.js
# Execute the Node.js script directly # Execute the Node.js script directly
npm run start npm run start

View File

@@ -36,7 +36,12 @@ class AxiosClient {
} }
// Generic method to make any Axios request // Generic method to make any Axios request
public async request(config: AxiosRequestConfig): Promise<AxiosResponse> { public async request(config: AxiosRequestConfig, bypassProxy = false): Promise<AxiosResponse> {
if (bypassProxy) {
const bypassInstance = axios.create()
return bypassInstance.request(config)
}
return this.instance.request(config) return this.instance.request(config)
} }
} }

View File

@@ -19,7 +19,9 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
const cleanStr = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}` const cleanStr = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`
// Send the clean string to the Webhook // Send the clean string to the Webhook
Webhook(configData, cleanStr) if (!configData.webhookLogExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
Webhook(configData, cleanStr)
}
// Formatted string with chalk for terminal logging // Formatted string with chalk for terminal logging
const str = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${chalkedPlatform} [${title}] ${message}` const str = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${chalkedPlatform} [${title}] ${message}`
@@ -40,4 +42,4 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
applyChalk ? console.log(applyChalk(str)) : console.log(str) applyChalk ? console.log(applyChalk(str)) : console.log(str)
break break
} }
} }

View File

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