* 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
This commit is contained in:
Light
2025-10-15 16:12:15 +02:00
committed by GitHub
parent dc7e122bce
commit 4d928d7dd9
25 changed files with 2830 additions and 815 deletions

View File

@@ -321,16 +321,8 @@ export class Login {
}
await input.fill('')
await input.fill(code)
const submitSelectors = [
'#idSubmit_SAOTCC_Continue',
'#idSubmit_SAOTCC_OTC',
'button[type="submit"]:has-text("Verify")',
'button[type="submit"]:has-text("Continuer")',
'button:has-text("Verify")',
'button:has-text("Continuer")',
'button:has-text("Submit")'
]
const submit = await this.findFirstVisibleLocator(page, submitSelectors)
// Use unified selector system
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
if (submit) {
await submit.click().catch(()=>{})
} else {
@@ -342,19 +334,17 @@ export class Login {
}
}
private totpInputSelectors(): string[] {
return [
// Unified selector system - DRY principle
private static readonly TOTP_SELECTORS = {
input: [
'input[name="otc"]',
'#idTxtBx_SAOTCC_OTC',
'#idTxtBx_SAOTCS_OTC',
'input[data-testid="otcInput"]',
'input[autocomplete="one-time-code"]',
'input[type="tel"][name="otc"]'
]
}
private totpAltOptionSelectors(): string[] {
return [
],
altOptions: [
'#idA_SAOTCS_ProofPickerChange',
'#idA_SAOTCC_AlternateLogin',
'a:has-text("Use a different verification option")',
@@ -362,11 +352,8 @@ export class Login {
'a:has-text("I can\'t use my Microsoft Authenticator app right now")',
'button:has-text("Use a different verification option")',
'button:has-text("Sign in another way")'
]
}
private totpChallengeSelectors(): string[] {
return [
],
challenge: [
'[data-value="PhoneAppOTP"]',
'[data-value="OneTimeCode"]',
'button:has-text("Use a verification code")',
@@ -380,20 +367,32 @@ export class Login {
'button:has-text("Entrez un code")',
'div[role="button"]:has-text("Use a verification code")',
'div[role="button"]:has-text("Enter a code")'
],
submit: [
'#idSubmit_SAOTCC_Continue',
'#idSubmit_SAOTCC_OTC',
'button[type="submit"]:has-text("Verify")',
'button[type="submit"]:has-text("Continuer")',
'button:has-text("Verify")',
'button:has-text("Continuer")',
'button:has-text("Submit")'
]
}
} as const
private async findFirstVisibleSelector(page: Page, selectors: string[]): Promise<string | null> {
private totpInputSelectors(): readonly string[] { return Login.TOTP_SELECTORS.input }
private totpAltOptionSelectors(): readonly string[] { return Login.TOTP_SELECTORS.altOptions }
private totpChallengeSelectors(): readonly string[] { return Login.TOTP_SELECTORS.challenge }
// Generic selector finder - reduces duplication from 3 functions to 1
private async findFirstVisibleSelector(page: Page, selectors: readonly string[]): Promise<string | null> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
return sel
}
if (await loc.isVisible().catch(() => false)) return sel
}
return null
}
private async clickFirstVisibleSelector(page: Page, selectors: string[]): Promise<boolean> {
private async clickFirstVisibleSelector(page: Page, selectors: readonly string[]): Promise<boolean> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
@@ -404,12 +403,10 @@ export class Login {
return false
}
private async findFirstVisibleLocator(page: Page, selectors: string[]): Promise<Locator | null> {
private async findFirstVisibleLocator(page: Page, selectors: readonly string[]): Promise<Locator | null> {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
return loc
}
if (await loc.isVisible().catch(() => false)) return loc
}
return null
}

View File

@@ -147,95 +147,98 @@ export class Workers {
// Solve all the different types of activities
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
const activityInitial = activityPage.url()
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
for (const activity of activities) {
for (const activity of activities) {
try {
// Reselect the worker page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
activityPage = await this.manageTabLifecycle(activityPage, activityInitial)
await this.applyThrottle(throttle, 800, 1400)
const pages = activityPage.context().pages()
if (pages.length > 3) {
await activityPage.close()
const selector = await this.buildActivitySelector(activityPage, activity, punchCard)
await this.prepareActivityPage(activityPage, selector, throttle)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
}
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(800*m), Math.floor(1400*m))
}
if (activityPage.url() !== activityInitial) {
await activityPage.goto(activityInitial)
}
let selector = `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
if (punchCard) {
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} else if (activity.name.toLowerCase().includes('membercenter') || activity.name.toLowerCase().includes('exploreonbing')) {
selector = `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
}
// Wait for the new tab to fully load, ignore error.
/*
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load.
*/
await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
// Small human-like jitter before executing
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
// Log the detected type using the same heuristics as before
const typeLabel = this.bot.activities.getTypeLabel(activity)
if (typeLabel !== 'Unsupported') {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${typeLabel}" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Watchdog: abort if the activity hangs too long
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(activityPage, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
await this.executeActivity(activityPage, activity, selector, throttle, retry)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
}
// Cooldown with jitter
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
await this.applyThrottle(throttle, 1200, 2600)
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_error_${activity.title || activity.offerId}`)
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
throttle.record(false)
}
}
}
private async manageTabLifecycle(page: Page, initialUrl: string): Promise<Page> {
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close()
page = await this.bot.browser.utils.getLatestTab(page)
}
if (page.url() !== initialUrl) {
await page.goto(initialUrl)
}
return page
}
private async buildActivitySelector(page: Page, activity: PromotionalItem | MorePromotion, punchCard?: PunchCard): Promise<string> {
if (punchCard) {
return await this.bot.browser.func.getPunchCardActivity(page, activity)
}
const name = activity.name.toLowerCase()
if (name.includes('membercenter') || name.includes('exploreonbing')) {
return `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
}
return `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
}
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.humanizePage(page)
await this.applyThrottle(throttle, 1200, 2600)
}
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
await page.click(selector)
page = await this.bot.browser.utils.getLatestTab(page)
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(page, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(page, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
await this.bot.browser.utils.humanizePage(page)
}
private async applyThrottle(throttle: AdaptiveThrottler, min: number, max: number): Promise<void> {
const multiplier = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(min * multiplier), Math.floor(max * multiplier))
}
}

View File

@@ -20,6 +20,14 @@ export class Quiz extends Workers {
await this.bot.utils.wait(2000)
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)
if (!firstOptionExists) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz options not found - page may not have loaded correctly. Skipping.', 'warn')
await page.close()
return
}
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
// All questions
@@ -29,13 +37,26 @@ 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 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found - quiz structure may have changed. Skipping remaining options.`, 'warn')
break
}
const answerAttribute = await answerSelector?.evaluate((el: Element) => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
answers.push(`#rqAnswerOption${i}`)
}
}
// If no correct answers found, skip this question
if (answers.length === 0) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'No correct answers found for 8-option quiz. Skipping.', 'warn')
await page.close()
return
}
// Click the answers
for (const answer of answers) {
@@ -56,15 +77,24 @@ export class Quiz extends Workers {
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
const correctOption = quizData.correctAnswer
let answerClicked = false
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
continue
}
const dataOption = await answerSelector?.evaluate((el: Element) => el.getAttribute('data-option'))
if (dataOption === correctOption) {
// Click the answer on page
await page.click(`#rqAnswerOption${i}`)
answerClicked = true
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
@@ -72,8 +102,16 @@ export class Quiz extends Workers {
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
break
}
}
if (!answerClicked) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Could not find correct answer for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
await page.close()
return
}
await this.bot.utils.wait(2000)
}
}