mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
267 lines
12 KiB
TypeScript
267 lines
12 KiB
TypeScript
import { load } from 'cheerio'
|
|
import { Page } from 'rebrowser-playwright'
|
|
import { MicrosoftRewardsBot } from '../index'
|
|
import { waitForPageReady } from '../util/browser/SmartWait'
|
|
import { logError } from '../util/notifications/Logger'
|
|
|
|
type DismissButton = { selector: string; label: string; isXPath?: boolean }
|
|
|
|
export default class BrowserUtil {
|
|
private bot: MicrosoftRewardsBot
|
|
|
|
private static readonly DISMISS_BUTTONS: readonly DismissButton[] = [
|
|
{ selector: '#acceptButton', label: 'AcceptButton' },
|
|
{ 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 }
|
|
]
|
|
|
|
private static readonly OVERLAY_SELECTORS = {
|
|
container: '#bnp_overlay_wrapper',
|
|
reject: '#bnp_btn_reject, button[aria-label*="Reject" i]',
|
|
accept: '#bnp_btn_accept'
|
|
} as const
|
|
|
|
private static readonly STREAK_DIALOG_SELECTORS = {
|
|
container: '[role="dialog"], div[role="alert"], div.ms-Dialog',
|
|
textFilter: /streak protection has run out/i,
|
|
closeButtons: 'button[aria-label*="close" i], button:has-text("Close"), button:has-text("Dismiss"), button:has-text("Got it"), button:has-text("OK"), button:has-text("Ok")'
|
|
} as const
|
|
|
|
private static readonly TERMS_UPDATE_SELECTORS = {
|
|
titleId: '#iTOUTitle',
|
|
titleText: /we're updating our terms/i,
|
|
nextButton: 'button[data-testid="primaryButton"]:has-text("Next"), button[type="submit"]:has-text("Next")'
|
|
} as const
|
|
|
|
constructor(bot: MicrosoftRewardsBot) {
|
|
this.bot = bot
|
|
}
|
|
|
|
async tryDismissAllMessages(page: Page): Promise<void> {
|
|
// Single-pass dismissal with all checks combined
|
|
await this.dismissAllInterruptors(page)
|
|
}
|
|
|
|
private async dismissAllInterruptors(page: Page): Promise<void> {
|
|
await Promise.allSettled([
|
|
this.dismissStandardButtons(page),
|
|
this.dismissOverlayButtons(page),
|
|
this.dismissStreakDialog(page),
|
|
this.dismissTermsUpdateDialog(page)
|
|
])
|
|
}
|
|
|
|
private async dismissStandardButtons(page: Page): Promise<number> {
|
|
let count = 0
|
|
for (const btn of BrowserUtil.DISMISS_BUTTONS) {
|
|
const dismissed = await this.tryClickButton(page, btn)
|
|
if (dismissed) {
|
|
count++
|
|
await page.waitForTimeout(150)
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
private async tryClickButton(page: Page, btn: DismissButton): Promise<boolean> {
|
|
try {
|
|
const loc = btn.isXPath ? page.locator(`xpath=${btn.selector}`) : page.locator(btn.selector)
|
|
const visible = await loc.first().isVisible({ timeout: 200 }).catch(() => false)
|
|
if (!visible) return false
|
|
|
|
await loc.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', `Failed to click ${btn.label}`, this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
|
|
return true
|
|
} catch (e) {
|
|
// Silent catch is intentional: button detection/click failures shouldn't break page flow
|
|
// Most failures are expected (button not present, timing issues, etc.)
|
|
return false
|
|
}
|
|
}
|
|
|
|
private async dismissOverlayButtons(page: Page): Promise<number> {
|
|
try {
|
|
const { container, reject, accept } = BrowserUtil.OVERLAY_SELECTORS
|
|
const overlay = page.locator(container)
|
|
const visible = await overlay.isVisible({ timeout: 200 }).catch(() => false)
|
|
if (!visible) return 0
|
|
|
|
const rejectBtn = overlay.locator(reject)
|
|
if (await rejectBtn.first().isVisible().catch(() => false)) {
|
|
await rejectBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay reject click failed', this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
|
|
return 1
|
|
}
|
|
|
|
const acceptBtn = overlay.locator(accept)
|
|
if (await acceptBtn.first().isVisible().catch(() => false)) {
|
|
await acceptBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay accept click failed', this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
} catch (e) {
|
|
// Silent catch is intentional: overlay detection failures are expected when no overlay present
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private async dismissStreakDialog(page: Page): Promise<number> {
|
|
try {
|
|
const { container, textFilter, closeButtons } = BrowserUtil.STREAK_DIALOG_SELECTORS
|
|
const dialog = page.locator(container).filter({ hasText: textFilter })
|
|
const visible = await dialog.first().isVisible({ timeout: 200 }).catch(() => false)
|
|
if (!visible) return 0
|
|
|
|
const closeBtn = dialog.locator(closeButtons).first()
|
|
if (await closeBtn.isVisible({ timeout: 200 }).catch(() => false)) {
|
|
await closeBtn.click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Streak dialog close failed', this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Button')
|
|
return 1
|
|
}
|
|
|
|
await page.keyboard.press('Escape').catch(logError('BROWSER-UTIL', 'Streak dialog Escape failed', this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Escape')
|
|
return 1
|
|
} catch (e) {
|
|
// Silent catch is intentional: streak dialog detection failures are expected
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private async dismissTermsUpdateDialog(page: Page): Promise<number> {
|
|
try {
|
|
const { titleId, titleText, nextButton } = BrowserUtil.TERMS_UPDATE_SELECTORS
|
|
|
|
// Check if terms update page is present
|
|
const titleById = page.locator(titleId)
|
|
const titleByText = page.locator('h1').filter({ hasText: titleText })
|
|
|
|
const hasTitle = await titleById.isVisible({ timeout: 200 }).catch(() => false) ||
|
|
await titleByText.first().isVisible({ timeout: 200 }).catch(() => false)
|
|
|
|
if (!hasTitle) return 0
|
|
|
|
// Click the Next button
|
|
const nextBtn = page.locator(nextButton).first()
|
|
if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile))
|
|
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)')
|
|
// Wait a bit for navigation
|
|
await page.waitForTimeout(1000)
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
} catch (e) {
|
|
// Silent catch is intentional: terms dialog detection failures are expected
|
|
return 0
|
|
}
|
|
}
|
|
|
|
async getLatestTab(page: Page): Promise<Page> {
|
|
try {
|
|
await this.bot.utils.wait(1000)
|
|
|
|
const browser = page.context()
|
|
const pages = browser.pages()
|
|
|
|
// IMPROVED: If no pages exist, create a new one instead of throwing error
|
|
if (pages.length === 0) {
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'No pages found in context, creating new page', 'warn')
|
|
const newPage = await browser.newPage()
|
|
await this.bot.utils.wait(500)
|
|
return newPage
|
|
}
|
|
|
|
const newTab = pages[pages.length - 1]
|
|
|
|
// IMPROVED: Verify the page is not closed before returning
|
|
if (newTab && !newTab.isClosed()) {
|
|
return newTab
|
|
}
|
|
|
|
// IMPROVED: If latest tab is closed, find first non-closed tab or create new one
|
|
const openPage = pages.find(p => !p.isClosed())
|
|
if (openPage) {
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Latest tab was closed, using first available open tab')
|
|
return openPage
|
|
}
|
|
|
|
// IMPROVED: Last resort - create new page
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'All tabs were closed, creating new page', 'warn')
|
|
const newPage = await browser.newPage()
|
|
await this.bot.utils.wait(500)
|
|
return newPage
|
|
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Critical error in getLatestTab: ' + errorMessage, 'error')
|
|
|
|
// IMPROVED: Try one more time to create a new page as absolute last resort
|
|
try {
|
|
const browser = page.context()
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Attempting recovery by creating new page', 'warn')
|
|
const recoveryPage = await browser.newPage()
|
|
await this.bot.utils.wait(500)
|
|
return recoveryPage
|
|
} catch (recoveryError) {
|
|
const recoveryMsg = recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
|
|
this.bot.log(this.bot.isMobile, 'GET-NEW-TAB', 'Recovery failed: ' + recoveryMsg, 'error')
|
|
throw new Error('Get new tab failed and recovery unsuccessful: ' + errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
async reloadBadPage(page: Page): Promise<void> {
|
|
try {
|
|
const html = await page.content().catch(() => '')
|
|
const $ = load(html)
|
|
|
|
const isNetworkError = $('body.neterror').length
|
|
const hasHttp400Error = html.includes('HTTP ERROR 400') ||
|
|
html.includes('This page isn\'t working') ||
|
|
html.includes('This page is not working')
|
|
|
|
if (isNetworkError || hasHttp400Error) {
|
|
const errorType = hasHttp400Error ? 'HTTP 400' : 'network error'
|
|
this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', `Bad page detected (${errorType}), reloading!`)
|
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
// IMPROVED: Use smart wait instead of fixed 1500ms delay
|
|
// FIXED: Use default 10s timeout for page reload
|
|
await waitForPageReady(page)
|
|
}
|
|
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
this.bot.log(this.bot.isMobile, 'RELOAD-BAD-PAGE', 'An error occurred: ' + errorMessage, 'error')
|
|
throw new Error('Reload bad page failed: ' + errorMessage)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
await this.bot.humanizer.microGestures(page)
|
|
await this.bot.humanizer.actionPause()
|
|
} catch { /* swallow */ }
|
|
}
|
|
|
|
} |