V2.3.0 Optimization (#380)

* Updated README.md to reflect version 2.1 and improve the presentation of Microsoft Rewards Automation features.

* Updated version to 2.1.5 in README.md and package.json, added new license and legal notice sections, and improved the configuration script for a better user experience.

* Mise à jour des messages de journalisation et ajout de vérifications pour le chargement des quiz et la présence des options avant de procéder. Suppression de fichiers de configuration obsolètes.

* Added serial protection dialog management for message forwarding, including closing by button or escape.

* feat: Implement BanPredictor for predicting ban risks based on historical data and real-time events

feat: Add ConfigValidator to validate configuration files and catch common issues

feat: Create QueryDiversityEngine to fetch diverse search queries from multiple sources

feat: Develop RiskManager to monitor account activity and assess risk levels dynamically

* Refactor code for consistency and readability; unify string quotes, improve logging with contextual emojis, enhance configuration validation, and streamline risk management logic.

* feat: Refactor BrowserUtil and Login classes for improved button handling and selector management; implement unified selector system and enhance activity processing logic in Workers class.

* feat: Improve logging with ASCII context icons for better compatibility with Windows PowerShell

* feat: Add sample account setup

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* feat: Update Node.js engine requirement to >=20.0.0 and improve webhook avatar handling and big fix Schedule

* Update README.md

* feat: Improve logging for Google Trends search queries and adjust fallback condition

* feat: Update version to 2.2.1 and enhance dashboard data retrieval with improved error handling

* feat: Update version to 2.2.2 and add terms update dialog dismissal functionality

* feat: Update version to 2.2.2 and require Node.js engine >=20.0.0

* feat: Ajouter un fichier de configuration complet pour la gestion des tâches et des performances

* feat: Mettre à jour la version à 2.2.3, modifier le fuseau horaire par défaut et activer les rapports d'analyse

* feat: update doc

* feat: update doc

* Refactor documentation for proxy setup, security guide, and auto-update system

- Updated proxy documentation to streamline content and improve clarity.
- Revised security guide to emphasize best practices and incident response.
- Simplified auto-update documentation, enhancing user understanding of the update process.
- Removed redundant sections and improved formatting for better readability.

* feat: update version to 2.2.7 in package.json

* feat: update version to 2.2.7 in README.md

* feat: improve quiz data retrieval with alternative variables and debug logs

* feat: refactor timeout and selector constants for improved maintainability

* feat: update version to 2.2.8 in package.json and add retry limits in constants

* feat: enhance webhook logging with username, avatar, and color-coded messages

* feat: update .gitignore to include diagnostic folder and bump version to 2.2.8 in package-lock.json

* feat: updated version to 2.3.0 and added new constants to improve the handling of delays and colors in logs
This commit is contained in:
Light
2025-10-16 17:59:53 +02:00
committed by GitHub
parent 4d928d7dd9
commit abd6117db3
37 changed files with 2392 additions and 4104 deletions

View File

@@ -202,6 +202,10 @@ export class Login {
// --------------- 2FA Handling ---------------
private async handle2FA(page: Page) {
try {
// Dismiss any popups/dialogs before checking 2FA (Terms Update, etc.)
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(500)
if (this.currentTotpSecret) {
const totpSelector = await this.ensureTotpInput(page)
if (totpSelector) {
@@ -273,10 +277,45 @@ export class Login {
} catch {/* ignore */}
}
// Manual prompt
// Manual prompt with periodic page check
this.bot.log(this.bot.isMobile, 'LOGIN', 'Waiting for user 2FA code (SMS / Email / App fallback)')
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const code: string = await new Promise(res => rl.question('Enter 2FA code:\n', ans => { rl.close(); res(ans.trim()) }))
// Monitor page changes while waiting for user input
let userInput: string | null = null
let checkInterval: NodeJS.Timeout | null = null
const inputPromise = new Promise<string>(res => {
rl.question('Enter 2FA code:\n', ans => {
if (checkInterval) clearInterval(checkInterval)
rl.close()
res(ans.trim())
})
})
// Check every 2 seconds if user manually progressed past the dialog
checkInterval = setInterval(async () => {
try {
await this.bot.browser.utils.tryDismissAllMessages(page)
// Check if we're no longer on 2FA page
const still2FA = await page.locator('input[name="otc"]').first().isVisible({ timeout: 500 }).catch(() => false)
if (!still2FA) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Page changed during 2FA wait (user may have clicked Next)', 'warn')
if (checkInterval) clearInterval(checkInterval)
rl.close()
userInput = 'skip' // Signal to skip submission
}
} catch {/* ignore */}
}, 2000)
const code = await inputPromise
if (checkInterval) clearInterval(checkInterval)
if (code === 'skip' || userInput === 'skip') {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Skipping 2FA code submission (page progressed)')
return
}
await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code submitted')
@@ -729,7 +768,7 @@ export class Login {
}
private getDocsUrl(anchor?: string) {
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/blob/V2/docs/security.md'
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/blob/v2/docs/security.md'
const map: Record<string,string> = {
'recovery-email-mismatch':'#recovery-email-mismatch',
'we-cant-sign-you-in':'#we-cant-sign-you-in-blocked'

View File

@@ -1,6 +1,7 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { RETRY_LIMITS, TIMEOUTS } from '../../constants'
export class ABC extends Workers {
@@ -11,34 +12,32 @@ export class ABC extends Workers {
try {
let $ = await this.bot.browser.func.loadInCheerio(page)
// Don't loop more than 15 in case unable to solve, would lock otherwise
const maxIterations = 15
let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10000 })
for (i = 0; i < RETRY_LIMITS.ABC_MAX && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10000 })
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
await this.bot.utils.wait(2000)
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page)
$ = await this.bot.browser.func.loadInCheerio(page)
await this.bot.utils.wait(1000)
await this.bot.utils.wait(TIMEOUTS.MEDIUM)
}
await this.bot.utils.wait(4000)
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.close()
if (i === maxIterations) {
this.bot.log(this.bot.isMobile, 'ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
if (i === RETRY_LIMITS.ABC_MAX) {
this.bot.log(this.bot.isMobile, 'ABC', `Failed to solve quiz, exceeded max iterations of ${RETRY_LIMITS.ABC_MAX}`, 'warn')
} else {
this.bot.log(this.bot.isMobile, 'ABC', 'Completed the ABC successfully')
}

View File

@@ -1,6 +1,7 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { TIMEOUTS } from '../../constants'
export class Poll extends Workers {
@@ -11,12 +12,14 @@ export class Poll extends Workers {
try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10000 }).catch(() => { }) // We're gonna click regardless or not
await this.bot.utils.wait(2000)
await page.waitForSelector(buttonId, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((e) => {
this.bot.log(this.bot.isMobile, 'POLL', `Could not find poll button: ${e}`, 'warn')
})
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
await page.click(buttonId)
await this.bot.utils.wait(4000)
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.close()
this.bot.log(this.bot.isMobile, 'POLL', 'Completed the poll successfully')

View File

@@ -1,6 +1,7 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { RETRY_LIMITS, TIMEOUTS, DELAYS } from '../../constants'
export class Quiz extends Workers {
@@ -10,19 +11,19 @@ export class Quiz extends Workers {
try {
// Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: TIMEOUTS.MEDIUM_LONG }).then(() => true).catch(() => false)
if (quizNotStarted) {
await page.click('#rqStartQuiz')
} else {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz has already been started, trying to finish it')
}
await this.bot.utils.wait(2000)
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
let quizData = await this.bot.browser.func.getQuizData(page)
// Verify quiz is actually loaded before proceeding
const firstOptionExists = await page.waitForSelector('#rqAnswerOption0', { state: 'attached', timeout: 5000 }).then(() => true).catch(() => false)
const firstOptionExists = await page.waitForSelector('#rqAnswerOption0', { state: 'attached', timeout: TIMEOUTS.VERY_LONG }).then(() => true).catch(() => false)
if (!firstOptionExists) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz options not found - page may not have loaded correctly. Skipping.', 'warn')
await page.close()
@@ -37,7 +38,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: 10000 }).catch(() => null)
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found - quiz structure may have changed. Skipping remaining options.`, 'warn')
@@ -60,7 +61,7 @@ export class Quiz extends Workers {
// Click the answers
for (const answer of answers) {
await page.waitForSelector(answer, { state: 'visible', timeout: 2000 })
await page.waitForSelector(answer, { state: 'visible', timeout: DELAYS.QUIZ_ANSWER_WAIT })
// Click the answer on page
await page.click(answer)
@@ -82,7 +83,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 }).catch(() => null)
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: RETRY_LIMITS.QUIZ_ANSWER_TIMEOUT }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
@@ -112,12 +113,12 @@ export class Quiz extends Workers {
return
}
await this.bot.utils.wait(2000)
await this.bot.utils.wait(DELAYS.QUIZ_ANSWER_WAIT)
}
}
// Done with
await this.bot.utils.wait(2000)
await this.bot.utils.wait(DELAYS.QUIZ_ANSWER_WAIT)
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'Completed the quiz successfully')

View File

@@ -277,8 +277,10 @@ export class Search extends Workers {
}
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')
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Found ${mappedTrendsData.length} search queries for ${geoLocale}`)
if (mappedTrendsData.length < 30 && geoLocale.toUpperCase() !== 'US') {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Insufficient search queries (${mappedTrendsData.length} < 30), falling back to US`, 'warn')
return this.getGoogleTrends()
}

View File

@@ -3,6 +3,7 @@ import * as fs from 'fs'
import path from 'path'
import { Workers } from '../Workers'
import { DELAYS } from '../../constants'
import { MorePromotion, PromotionalItem } from '../../interface/DashboardData'
@@ -13,7 +14,7 @@ export class SearchOnBing extends Workers {
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Trying to complete SearchOnBing')
try {
await this.bot.utils.wait(5000)
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_WAIT)
await this.bot.browser.utils.tryDismissAllMessages(page)
@@ -21,20 +22,20 @@ export class SearchOnBing extends Workers {
const searchBar = '#sb_form_q'
const box = page.locator(searchBar)
await box.waitFor({ state: 'attached', timeout: 15000 })
await box.waitFor({ state: 'attached', timeout: DELAYS.SEARCH_BAR_TIMEOUT })
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(200)
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS)
try {
await box.focus({ timeout: 2000 }).catch(() => { /* ignore */ })
await box.focus({ timeout: DELAYS.THIS_OR_THAT_START }).catch(() => { /* ignore */ })
await box.fill('')
await this.bot.utils.wait(200)
await page.keyboard.type(query, { delay: 20 })
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS)
await page.keyboard.type(query, { delay: DELAYS.TYPING_DELAY })
await page.keyboard.press('Enter')
} catch {
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}`
await page.goto(url)
}
await this.bot.utils.wait(3000)
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_COMPLETE)
await page.close()

View File

@@ -1,6 +1,7 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { DELAYS } from '../../constants'
export class ThisOrThat extends Workers {
@@ -11,14 +12,14 @@ export class ThisOrThat extends Workers {
try {
// Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: DELAYS.THIS_OR_THAT_START }).then(() => true).catch(() => false)
if (quizNotStarted) {
await page.click('#rqStartQuiz')
} else {
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
}
await this.bot.utils.wait(2000)
await this.bot.utils.wait(DELAYS.THIS_OR_THAT_START)
// Solving
const quizData = await this.bot.browser.func.getQuizData(page)

View File

@@ -9,7 +9,7 @@ export class UrlReward extends Workers {
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Trying to complete UrlReward')
try {
this.bot.utils.wait(2000)
await this.bot.utils.wait(2000)
await page.close()