mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-20 15:03:58 +00:00
V2 (#365)
* first commit * Addition of a personalized activity manager and refactoring of the logic of activities * Adding diagnostics management, including screenshot and HTML content, as well as improvements to humanize page interactions and +. * Adding the management of newspapers and webhook settings, including filtering messages and improving the structure of the summaries sent. * Adding a post-execution auto-date functionality, including options to update via Git and Docker, as well as a new configuration interface to manage these parameters. * Adding accounts in Docker, with options to use an environmental file or online JSON data, as well as minimum validations for responsible accounts. * Improving the Microsoft Rewards script display with a new headband and better log management, including colors and improved formatting for the console. * v2 * Refactor ESLint configuration and scripts for improved TypeScript support and project structure * Addition of the detection of suspended accounts with the gesture of the improved errors and journalization of banishment reasons * Adding an integrated planner for programmed task execution, with configuration in Config.json and + * Edit * Remove texte * Updating of documentation and adding the management of humanization in the configuration and +. * Adding manual purchase method allowing users to spend points without automation, with monitoring of expenses and notifications. * Correction of documentation and improvement of configuration management for manual purchase mode, adding complete documentation and appropriate banner display. * Add comprehensive documentation for job state persistence, NTFY notifications, proxy configuration, scheduling, and auto-update features - Introduced job state persistence documentation to track progress and resume tasks. - Added NTFY push notifications integration guide for real-time alerts. - Documented proxy configuration options for enhanced privacy and network management. - Included scheduling configuration for automated script execution. - Implemented auto-update configuration to keep installations current with Git and Docker options. * Ajout d'Unt Système de Rapport d'Erreurs Communautaire pour Améliorerer le Débogage, incluant la Configuration et l'Envoi de Résumés D'Erreurs Anonyés à un webhook Discord. * Mini Edit * Mise à Jour du Readme.md pour Améliorerer la Présentation et La Claté, Ajout d'Un section sur les notifications en Temps Raine et Mise à Jour des badges pour la meille unibilité. * Documentation update * Edit README.md * Edit * Update README with legacy version link * Improvement of location data management and webhooks, adding configurations normalization * Force update for PR * Improvement of documentation and configuration options for Cron integration and Docker use * Improvement of planning documentation and adding a multi-pan-pancake in the daily execution script * Deletion of the CommunityReport functionality in accordance with the project policy * Addition of randomization of start -up schedules and surveillance time for planner executions * Refactor Docker setup to use built-in scheduler, removing cron dependencies and simplifying configuration options * Adding TOTP support for authentication, update of interfaces and configuration files to include Totp secret, and automatic generation of the Totp code when connecting. * Fix [LOGIN-NO-PROMPT] No dialogs (xX) * Reset the Totp field for email_1 in the accounts.example.json file * Reset the Totp field for email_1 in the Readme.md file * Improvement of Bing Research: Use of the 'Attacked' method for the research field, management of overlays and adding direct navigation in the event of entry failure. * Adding a complete security policy, including directives on vulnerability management, coordinated disclosure and user security advice. * Remove advanced environment variables section from README * Configuration and dockerfile update: Passage to Node 22, addition of management of the purchase method, deletion of obsolete scripts * Correction of the order of the sections in the Readme.md for better readability * Update of Readm and Security Policy: Addition of the method of purchase and clarification of security and confidentiality practices. * Improvement of the readability of the Readm and deletion of the mention of reporting of vulnerabilities in the security document. * Addition of humanization management and adaptive throttling to simulate more human behavior in bot activities. * Addition of humanization management: activation/deactivation of human gestures, configuration update and adding documentation on human mode. * Deletion of community error report functionality to respect the privacy policy * Addition of immediate banning alerts and vacation configuration in the Microsoft Rewards bot * Addition of immediate banning alerts and vacation configuration in the Microsoft Rewards bot * Added scheduling support: support for 12h and 24h formats, added options for time zone, and immediate execution on startup. * Added window size normalization and page rendering to fit typical screens, with injected CSS styles to prevent excessive zooming. * Added security incident management: detection of hidden recovery emails, automation blocking, and global alerts. Updated configuration files and interfaces to include recovery emails. Improved security incident documentation. * Refactor incident alert handling: unified alert sender * s * Added security incident management: detect recovery email inconsistencies and send unified alerts. Implemented helper methods to manage alerts and compromised modes. * Added heartbeat management for the scheduler: integrated a heartbeat file to report liveliness and adjusted the watchdog configuration to account for heartbeat updates. * Edit webook * Updated security alert management: fixed the recovery email hidden in the documentation and enabled the conclusion webhook for notifications. * Improved security alert handling: added structured sending to webhooks for better visibility and updated callback interval in compromised mode. * Edit conf * Improved dependency installation: Added the --ignore-scripts option for npm ci and npm install. Updated comments in compose.yaml for clarity. * Refactor documentation structure and enhance logging: - Moved documentation files from 'information' to 'docs' directory for better organization. - Added live logging configuration to support webhook logs with email redaction. - Updated file paths in configuration and loading functions to accommodate new structure. - Adjusted scheduler behavior to prevent immediate runs unless explicitly set. - Improved error handling for account and config file loading. - Enhanced security incident documentation with detailed recovery steps. * Fix docs * Remove outdated documentation on NTFY, Proxy, Scheduling, Security, and Auto-Update configurations; update Browser class to prioritize headless mode based on environment variable. * Addition of documentation for account management and Totp, Docker Guide, and Update of the Documentation Index. * Updating Docker documentation: simplification of instructions and adding links to detailed guides. Revision of configuration options and troubleshooting sections. * Edit * Edit docs * Enhance documentation for Scheduler, Security, and Auto-Update features - Revamped the Scheduler documentation to include detailed features, configuration options, and usage examples. - Expanded the Security guide with comprehensive incident response strategies, privacy measures, and monitoring practices. - Updated the Auto-Update section to clarify configuration, methods, and best practices for maintaining system integrity. * Improved error handling and added crash recovery in the Microsoft Rewards bot. Added configuration for automatic restart and handling of local search queries when trends fail. * Fixed initial point counting in MicrosoftRewardsBot and improved error handling when sending summaries to webhooks. * Added unified support for notifications and improved handling of webhook configurations in the normalizeConfig and log functions. * UPDATE LOGIN * EDIT LOGIN * Improved login error handling: added recovery mismatch detection and the ability to switch to password authentication. * Added a full reference to configuration in the documentation and improved log and error handling in the code. * Added context management for conclusion webhooks and improved user configuration for notifications. * Mini edit * Improved logic for extracting masked emails for more accurate matching during account recovery.
This commit is contained in:
@@ -29,16 +29,25 @@ class Browser {
|
||||
if (process.env.AUTO_INSTALL_BROWSERS === '1') {
|
||||
try {
|
||||
// Dynamically import child_process to avoid overhead otherwise
|
||||
const { execSync } = await import('child_process') as any
|
||||
const { execSync } = await import('child_process')
|
||||
execSync('npx playwright install chromium', { stdio: 'ignore' })
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
let browser: any
|
||||
let browser: import('rebrowser-playwright').Browser
|
||||
// Support both legacy and new config structures (wider scope for later usage)
|
||||
const cfgAny = this.bot.config as unknown as Record<string, unknown>
|
||||
try {
|
||||
// FORCE_HEADLESS env takes precedence (used in Docker with headless shell only)
|
||||
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
|
||||
const headlessValue = envForceHeadless ? true : ((cfgAny['headless'] as boolean | undefined) ?? (cfgAny['browser'] && (cfgAny['browser'] as Record<string, unknown>)['headless'] as boolean | undefined) ?? false)
|
||||
const headless: boolean = Boolean(headlessValue)
|
||||
|
||||
const engineName = 'chromium' // current hard-coded engine
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) // explicit engine log
|
||||
browser = await playwright.chromium.launch({
|
||||
//channel: 'msedge', // Uses Edge instead of chrome
|
||||
headless: this.bot.config.headless,
|
||||
headless,
|
||||
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
@@ -49,7 +58,7 @@ class Browser {
|
||||
'--ignore-ssl-errors'
|
||||
]
|
||||
})
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
const msg = (e instanceof Error ? e.message : String(e))
|
||||
// Common missing browser executable guidance
|
||||
if (/Executable doesn't exist/i.test(msg)) {
|
||||
@@ -60,18 +69,57 @@ class Browser {
|
||||
throw e
|
||||
}
|
||||
|
||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
|
||||
// Resolve saveFingerprint from legacy root or new fingerprinting.saveFingerprint
|
||||
const fpConfig = (cfgAny['saveFingerprint'] as unknown) || ((cfgAny['fingerprinting'] as Record<string, unknown> | undefined)?.['saveFingerprint'] as unknown)
|
||||
const saveFingerprint: { mobile: boolean; desktop: boolean } = (fpConfig as { mobile: boolean; desktop: boolean }) || { mobile: false, desktop: false }
|
||||
|
||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, saveFingerprint)
|
||||
|
||||
const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint()
|
||||
|
||||
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
|
||||
const context = await newInjectedContext(browser as unknown as import('playwright').Browser, { fingerprint: fingerprint })
|
||||
|
||||
// Set timeout to preferred amount
|
||||
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
|
||||
// Set timeout to preferred amount (supports legacy globalTimeout or browser.globalTimeout)
|
||||
const globalTimeout = (cfgAny['globalTimeout'] as unknown) ?? ((cfgAny['browser'] as Record<string, unknown> | undefined)?.['globalTimeout'] as unknown) ?? 30000
|
||||
context.setDefaultTimeout(this.bot.utils.stringToMs(globalTimeout as (number | string)))
|
||||
|
||||
// Normalize viewport and page rendering so content fits typical screens
|
||||
try {
|
||||
const desktopViewport = { width: 1280, height: 800 }
|
||||
const mobileViewport = { width: 390, height: 844 }
|
||||
|
||||
context.on('page', async (page) => {
|
||||
try {
|
||||
// Set a reasonable viewport size depending on device type
|
||||
if (this.bot.isMobile) {
|
||||
await page.setViewportSize(mobileViewport)
|
||||
} else {
|
||||
await page.setViewportSize(desktopViewport)
|
||||
}
|
||||
|
||||
// Inject a tiny CSS to avoid gigantic scaling on some environments
|
||||
await page.addInitScript(() => {
|
||||
try {
|
||||
const style = document.createElement('style')
|
||||
style.id = '__mrs_fit_style'
|
||||
style.textContent = `
|
||||
html, body { overscroll-behavior: contain; }
|
||||
/* Mild downscale to keep content within window on very large DPI */
|
||||
@media (min-width: 1000px) {
|
||||
html { zoom: 0.9 !important; }
|
||||
}
|
||||
`
|
||||
document.documentElement.appendChild(style)
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
|
||||
await context.addCookies(sessionData.cookies)
|
||||
|
||||
if (this.bot.config.saveFingerprint) {
|
||||
// Persist fingerprint when feature is configured
|
||||
if (fpConfig) {
|
||||
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,17 @@ export default class BrowserFunc {
|
||||
await this.bot.utils.wait(3000)
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
// Check if account is suspended
|
||||
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (isSuspended) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account is suspended!', 'error')
|
||||
// Check if account is suspended (multiple heuristics)
|
||||
const suspendedByHeader = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 1500 }).then(() => true).catch(() => false)
|
||||
let suspendedByText = false
|
||||
if (!suspendedByHeader) {
|
||||
try {
|
||||
const text = (await page.textContent('body')) || ''
|
||||
suspendedByText = /account has been suspended|suspended due to unusual activity/i.test(text)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
if (suspendedByHeader || suspendedByText) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account appears suspended!', 'error')
|
||||
throw new Error('Account has been suspended!')
|
||||
}
|
||||
|
||||
@@ -82,21 +89,22 @@ export default class BrowserFunc {
|
||||
* Fetch user dashboard data
|
||||
* @returns {DashboardData} Object of user bing rewards dashboard data
|
||||
*/
|
||||
async getDashboardData(): Promise<DashboardData> {
|
||||
async getDashboardData(page?: Page): Promise<DashboardData> {
|
||||
const target = page ?? this.bot.homePage
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
const currentURL = new URL(this.bot.homePage.url())
|
||||
const currentURL = new URL(target.url())
|
||||
|
||||
try {
|
||||
// Should never happen since tasks are opened in a new tab!
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||
await this.goHome(this.bot.homePage)
|
||||
await this.goHome(target)
|
||||
}
|
||||
let lastError: any = null
|
||||
let lastError: unknown = null
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
try {
|
||||
// Reload the page to get new data
|
||||
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
|
||||
await target.reload({ waitUntil: 'domcontentloaded' })
|
||||
lastError = null
|
||||
break
|
||||
} catch (re) {
|
||||
@@ -108,7 +116,7 @@ export default class BrowserFunc {
|
||||
if (attempt === 1) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
||||
try {
|
||||
await this.goHome(this.bot.homePage)
|
||||
await this.goHome(target)
|
||||
} catch {/* ignore */}
|
||||
} else {
|
||||
break
|
||||
@@ -119,7 +127,7 @@ export default class BrowserFunc {
|
||||
}
|
||||
}
|
||||
|
||||
const scriptContent = await this.bot.homePage.evaluate(() => {
|
||||
const scriptContent = await target.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
|
||||
@@ -131,7 +139,7 @@ export default class BrowserFunc {
|
||||
}
|
||||
|
||||
// Extract the dashboard object from the script content
|
||||
const dashboardData = await this.bot.homePage.evaluate((scriptContent: string) => {
|
||||
const dashboardData = await target.evaluate((scriptContent: string) => {
|
||||
// Extract the dashboard object using regex
|
||||
const regex = /var dashboard = (\{.*?\});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
@@ -232,8 +240,12 @@ export default class BrowserFunc {
|
||||
]
|
||||
|
||||
const data = await this.getDashboardData()
|
||||
let geoLocale = data.userProfile.attributes.country
|
||||
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||
// Guard against missing profile/attributes and undefined settings
|
||||
let geoLocale = data?.userProfile?.attributes?.country || 'US'
|
||||
const useGeo = !!(this.bot?.config?.searchSettings?.useGeoLocaleQueries)
|
||||
geoLocale = (useGeo && typeof geoLocale === 'string' && geoLocale.length === 2)
|
||||
? geoLocale.toLowerCase()
|
||||
: 'us'
|
||||
|
||||
const userDataRequest: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
|
||||
@@ -295,9 +307,10 @@ export default class BrowserFunc {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const scriptContent = $('script').filter((index: number, element: any) => {
|
||||
return $(element).text().includes('_w.rewardsQuizRenderInfo')
|
||||
}).text()
|
||||
const scriptContent = $('script')
|
||||
.toArray()
|
||||
.map(el => $(el).text())
|
||||
.find(t => t.includes('_w.rewardsQuizRenderInfo')) || ''
|
||||
|
||||
if (scriptContent) {
|
||||
const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s
|
||||
@@ -355,7 +368,10 @@ export default class BrowserFunc {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const element = $('.offer-cta').toArray().find((x: any) => x.attribs.href?.includes(activity.offerId))
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
if (element) {
|
||||
selector = `a[href*="${element.attribs.href}"]`
|
||||
}
|
||||
|
||||
@@ -12,52 +12,57 @@ export default class BrowserUtil {
|
||||
}
|
||||
|
||||
async tryDismissAllMessages(page: Page): Promise<void> {
|
||||
const buttons = [
|
||||
const attempts = 3
|
||||
const buttonGroups: { selector: string; label: string; isXPath?: boolean }[] = [
|
||||
{ selector: '#acceptButton', label: 'AcceptButton' },
|
||||
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
|
||||
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
|
||||
{ selector: '#iShowSkip', label: 'iShowSkip' },
|
||||
{ selector: '#iNext', label: 'iNext' },
|
||||
{ selector: '#iLooksGood', label: 'iLooksGood' },
|
||||
{ selector: '#idSIButton9', label: 'idSIButton9' },
|
||||
{ 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', isXPath: true },
|
||||
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
|
||||
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
|
||||
{ selector: '.optanon-allow-all, .optanon-alert-box-button', label: 'OneTrust Accept' },
|
||||
{ selector: '.ext-secondary.ext-button', label: 'Skip For Now' },
|
||||
{ selector: '#iLandingViewAction', label: 'Landing Continue' },
|
||||
{ selector: '#iShowSkip', label: 'Show Skip' },
|
||||
{ selector: '#iNext', label: 'Next' },
|
||||
{ selector: '#iLooksGood', label: 'LooksGood' },
|
||||
{ selector: '#idSIButton9', label: 'PrimaryLoginButton' },
|
||||
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Generic' },
|
||||
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Cancel' },
|
||||
{ selector: '.maybe-later, button[data-automation-id*="maybeLater" i]', label: 'Maybe Later' },
|
||||
{ selector: '#bnp_btn_reject', label: 'Bing Cookie Reject' },
|
||||
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Accept' },
|
||||
{ selector: '#bnp_close_link', label: 'Bing Cookie Close' },
|
||||
{ selector: '#reward_pivot_earn', label: 'Rewards Pivot Earn' },
|
||||
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Legacy Cookie Accept', isXPath: true }
|
||||
]
|
||||
|
||||
for (const button of buttons) {
|
||||
for (let round = 0; round < attempts; round++) {
|
||||
let dismissedThisRound = 0
|
||||
for (const btn of buttonGroups) {
|
||||
try {
|
||||
const loc = btn.isXPath ? page.locator(`xpath=${btn.selector}`) : page.locator(btn.selector)
|
||||
if (await loc.first().isVisible({ timeout: 200 }).catch(()=>false)) {
|
||||
await loc.first().click({ timeout: 500 }).catch(()=>{})
|
||||
dismissedThisRound++
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
|
||||
await page.waitForTimeout(150)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// Special case: blocking overlay with inside buttons
|
||||
try {
|
||||
const element = button.isXPath ? page.locator(`xpath=${button.selector}`) : page.locator(button.selector)
|
||||
await element.first().click({ timeout: 500 })
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
|
||||
|
||||
} catch (error) {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
// Handle blocking Bing privacy overlay intercepting clicks (#bnp_overlay_wrapper)
|
||||
try {
|
||||
const overlay = await page.locator('#bnp_overlay_wrapper').first()
|
||||
if (await overlay.isVisible({ timeout: 500 }).catch(()=>false)) {
|
||||
// Try common dismiss buttons inside overlay
|
||||
const rejectBtn = await page.locator('#bnp_btn_reject, button[aria-label*="Reject" i]').first()
|
||||
const acceptBtn = await page.locator('#bnp_btn_accept').first()
|
||||
if (await rejectBtn.isVisible().catch(()=>false)) {
|
||||
await rejectBtn.click({ timeout: 500 }).catch(()=>{})
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Reject')
|
||||
} else if (await acceptBtn.isVisible().catch(()=>false)) {
|
||||
await acceptBtn.click({ timeout: 500 }).catch(()=>{})
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Accept (fallback)')
|
||||
const overlay = page.locator('#bnp_overlay_wrapper')
|
||||
if (await overlay.isVisible({ timeout: 200 }).catch(()=>false)) {
|
||||
const reject = overlay.locator('#bnp_btn_reject, button[aria-label*="Reject" i]')
|
||||
const accept = overlay.locator('#bnp_btn_accept')
|
||||
if (await reject.first().isVisible().catch(()=>false)) {
|
||||
await reject.first().click({ timeout: 500 }).catch(()=>{})
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
|
||||
dismissedThisRound++
|
||||
} else if (await accept.first().isVisible().catch(()=>false)) {
|
||||
await accept.first().click({ timeout: 500 }).catch(()=>{})
|
||||
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
|
||||
dismissedThisRound++
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(300)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch { /* ignore */ }
|
||||
if (dismissedThisRound === 0) break // nothing new dismissed -> stop early
|
||||
}
|
||||
}
|
||||
|
||||
async getLatestTab(page: Page): Promise<Page> {
|
||||
@@ -78,40 +83,6 @@ export default class BrowserUtil {
|
||||
}
|
||||
}
|
||||
|
||||
async getTabs(page: Page) {
|
||||
try {
|
||||
const browser = page.context()
|
||||
const pages = browser.pages()
|
||||
|
||||
const homeTab = pages[1]
|
||||
let homeTabURL: URL
|
||||
|
||||
if (!homeTab) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Home tab could not be found!', 'error')
|
||||
|
||||
} else {
|
||||
homeTabURL = new URL(homeTab.url())
|
||||
|
||||
if (homeTabURL.hostname !== 'rewards.bing.com') {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const workerTab = pages[2]
|
||||
if (!workerTab) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Worker tab could not be found!', 'error')
|
||||
}
|
||||
|
||||
return {
|
||||
homeTab: homeTab,
|
||||
workerTab: workerTab
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async reloadBadPage(page: Page): Promise<void> {
|
||||
try {
|
||||
const html = await page.content().catch(() => '')
|
||||
@@ -129,4 +100,80 @@ export default class BrowserUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform small human-like gestures: short waits, minor mouse moves and occasional scrolls.
|
||||
* This should be called sparingly between actions to avoid a fixed cadence.
|
||||
*/
|
||||
async humanizePage(page: Page): Promise<void> {
|
||||
try {
|
||||
const h = this.bot.config?.humanization || {}
|
||||
if (h.enabled === false) return
|
||||
const moveProb = typeof h.gestureMoveProb === 'number' ? h.gestureMoveProb : 0.4
|
||||
const scrollProb = typeof h.gestureScrollProb === 'number' ? h.gestureScrollProb : 0.2
|
||||
// minor mouse move
|
||||
if (Math.random() < moveProb) {
|
||||
const x = Math.floor(Math.random() * 30) + 5
|
||||
const y = Math.floor(Math.random() * 20) + 3
|
||||
await page.mouse.move(x, y, { steps: 2 }).catch(() => { })
|
||||
}
|
||||
// tiny scroll
|
||||
if (Math.random() < scrollProb) {
|
||||
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
|
||||
await page.mouse.wheel(0, dy).catch(() => { })
|
||||
}
|
||||
// Random short wait; override via humanization.actionDelay
|
||||
const range = h.actionDelay
|
||||
if (range && typeof range.min !== 'undefined' && typeof range.max !== 'undefined') {
|
||||
try {
|
||||
const ms = (await import('ms')).default
|
||||
const min = typeof range.min === 'number' ? range.min : ms(String(range.min))
|
||||
const max = typeof range.max === 'number' ? range.max : ms(String(range.max))
|
||||
if (typeof min === 'number' && typeof max === 'number' && max >= min) {
|
||||
await this.bot.utils.wait(this.bot.utils.randomNumber(Math.max(0, min), Math.min(max, 5000)))
|
||||
} else {
|
||||
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
|
||||
}
|
||||
} catch {
|
||||
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
|
||||
}
|
||||
} else {
|
||||
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
|
||||
}
|
||||
} catch { /* swallow */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture minimal diagnostics for a page: screenshot + HTML content.
|
||||
* Files are written under ./reports/<date>/ with a safe label.
|
||||
*/
|
||||
async captureDiagnostics(page: Page, label: string): Promise<void> {
|
||||
try {
|
||||
const cfg = this.bot.config?.diagnostics || {}
|
||||
if (cfg.enabled === false) return
|
||||
const maxPerRun = typeof cfg.maxPerRun === 'number' ? cfg.maxPerRun : 8
|
||||
if (!this.bot.tryReserveDiagSlot(maxPerRun)) return
|
||||
|
||||
const safe = label.replace(/[^a-z0-9-_]/gi, '_').slice(0, 64)
|
||||
const now = new Date()
|
||||
const day = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`
|
||||
const baseDir = `${process.cwd()}/reports/${day}`
|
||||
const fs = await import('fs')
|
||||
const path = await import('path')
|
||||
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true })
|
||||
const ts = `${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}${String(now.getSeconds()).padStart(2,'0')}`
|
||||
const shot = path.join(baseDir, `${ts}_${safe}.png`)
|
||||
const htmlPath = path.join(baseDir, `${ts}_${safe}.html`)
|
||||
if (cfg.saveScreenshot !== false) {
|
||||
await page.screenshot({ path: shot }).catch(()=>{})
|
||||
}
|
||||
if (cfg.saveHtml !== false) {
|
||||
const html = await page.content().catch(()=> '<html></html>')
|
||||
fs.writeFileSync(htmlPath, html)
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'DIAG', `Saved diagnostics to ${shot} and ${htmlPath}`)
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'DIAG', `Failed to capture diagnostics: ${e instanceof Error ? e.message : e}`, 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user