Files
Microsoft-Rewards-Bot/src/browser/BrowserFunc.ts

765 lines
34 KiB
TypeScript

import { AxiosRequestConfig } from 'axios'
import { CheerioAPI, load } from 'cheerio'
import { BrowserContext, Page } from 'rebrowser-playwright'
import { RETRY_LIMITS, SELECTORS, TIMEOUTS, URLS } from '../constants'
import { MicrosoftRewardsBot } from '../index'
import { AppUserData } from '../interface/AppUserData'
import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
import { EarnablePoints } from '../interface/Points'
import { QuizData } from '../interface/QuizData'
import { waitForElementSmart, waitForPageReady } from '../util/browser/SmartWait'
import { saveSessionData } from '../util/state/Load'
export default class BrowserFunc {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
/**
* Check if account is suspended using multiple detection methods
* @param page Playwright page
* @param iteration Current iteration number for logging
* @returns true if suspended, false otherwise
*/
private async checkAccountSuspension(page: Page, iteration: number): Promise<boolean> {
// IMPROVED: Smart wait replaces fixed 500ms timeout with adaptive detection
const headerResult = await waitForElementSmart(page, SELECTORS.SUSPENDED_ACCOUNT, {
initialTimeoutMs: 500,
extendedTimeoutMs: 500,
state: 'visible'
})
const suspendedByHeader = headerResult.found
if (suspendedByHeader) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error')
return true
}
// Secondary check: look for suspension text in main content area only
try {
const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || ''
const suspensionPatterns = [
/account\s+has\s+been\s+suspended/i,
/suspended\s+due\s+to\s+unusual\s+activity/i,
/your\s+account\s+is\s+temporarily\s+suspended/i
]
const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent))
if (isSuspended) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
return true
}
} catch (error) {
// Ignore errors in text check - not critical
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn')
}
return false
}
/**
* Navigate the provided page to rewards homepage
* @param {Page} page Playwright page
*/
async goHome(page: Page) {
try {
const dashboardURL = new URL(this.bot.config.baseURL)
if (page.url() === dashboardURL.href) {
return
}
await page.goto(this.bot.config.baseURL)
// IMPROVED: Smart page readiness check after navigation
// FIXED: Use timeoutMs parameter with increased timeout for slower networks
const readyResult = await waitForPageReady(page, {
timeoutMs: 15000, // FIXED: 15s timeout to handle slower network conditions
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log')
})
if (readyResult.timeMs > 8000) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Page took ${readyResult.timeMs}ms to be ready (slow)`, 'warn')
}
// IMPROVED: Wait for Custom Elements to be registered with proper timeout handling
// FIXED: Use Promise.race to enforce actual 5s timeout (Playwright's timeout doesn't work with customElements.whenDefined)
try {
await Promise.race([
page.evaluate(() => customElements.whenDefined('mee-card-group')),
new Promise((_, reject) => setTimeout(() => reject(new Error('Custom element timeout')), 5000))
])
} catch (error) {
// FIXED: Silent fallback - custom element registration is best-effort, not critical
// If it times out, we proceed with activities detection anyway
this.bot.log(this.bot.isMobile, 'GO-HOME', 'mee-card-group custom element not registered within 5s (non-critical)', 'log')
}
for (let iteration = 1; iteration <= RETRY_LIMITS.GO_HOME_MAX; iteration++) {
await this.bot.utils.wait(500)
await this.bot.browser.utils.tryDismissAllMessages(page)
// IMPROVED: Try primary selector first with increased timeouts
let activitiesResult = await waitForElementSmart(page, SELECTORS.MORE_ACTIVITIES, {
initialTimeoutMs: 3000, // FIXED: Increased from 1000ms to 3000ms
extendedTimeoutMs: 7000, // FIXED: Increased from 2000ms to 7000ms
state: 'attached',
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log')
})
// IMPROVED: Try fallback selectors if primary fails
if (!activitiesResult.found) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Primary selector failed, trying fallbacks...', 'log')
for (const fallbackSelector of SELECTORS.MORE_ACTIVITIES_FALLBACKS) {
activitiesResult = await waitForElementSmart(page, fallbackSelector, {
initialTimeoutMs: 2000,
extendedTimeoutMs: 3000,
state: 'attached',
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log')
})
if (activitiesResult.found) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Found activities using fallback: ${fallbackSelector}`, 'log')
break
}
}
}
if (activitiesResult.found) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
break
}
// Activities not found yet - check if it's because account is suspended
const isSuspended = await this.checkAccountSuspension(page, iteration)
if (isSuspended) {
throw new Error('Account has been suspended!')
}
// IMPROVED: Enhanced diagnostic logging to identify DOM structure changes
if (iteration <= 2) {
try {
const diagnosticInfo = await page.evaluate(() => {
const elementsWithActivitiesId = document.querySelectorAll('[id*="activit"]')
const meeCardGroups = document.querySelectorAll('mee-card-group')
const hasRoleList = document.querySelectorAll('[role="list"]')
const dailySets = document.querySelectorAll('.daily-sets, [data-bi-name="daily-set"]')
const rewardsElements = document.querySelectorAll('[class*="rewards"], [id*="rewards"]')
const mainContent = document.querySelector('main')
return {
activitiesIdCount: elementsWithActivitiesId.length,
activitiesIds: Array.from(elementsWithActivitiesId).map(el => el.id).slice(0, 5),
meeCardGroupCount: meeCardGroups.length,
roleListCount: hasRoleList.length,
dailySetsCount: dailySets.length,
rewardsElementsCount: rewardsElements.length,
hasMainContent: !!mainContent,
pageTitle: document.title,
bodyClasses: document.body.className,
url: window.location.href
}
})
this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG',
'DOM Diagnostic - ' +
`URL: ${diagnosticInfo.url}, ` +
`Title: "${diagnosticInfo.pageTitle}", ` +
`Elements with 'activit': ${diagnosticInfo.activitiesIdCount} [${diagnosticInfo.activitiesIds.join(', ')}], ` +
`mee-card-group: ${diagnosticInfo.meeCardGroupCount}, ` +
`role=list: ${diagnosticInfo.roleListCount}, ` +
`daily-sets: ${diagnosticInfo.dailySetsCount}, ` +
`rewards elements: ${diagnosticInfo.rewardsElementsCount}, ` +
`main content: ${diagnosticInfo.hasMainContent}`, 'warn')
} catch (error) {
this.bot.log(this.bot.isMobile, 'GO-HOME-DEBUG', `Diagnostic failed: ${error}`, 'warn')
}
}
// IMPROVED: Capture screenshot on final iteration for debugging
if (iteration === RETRY_LIMITS.GO_HOME_MAX) {
try {
const fs = await import('fs')
const path = await import('path')
const debugDir = path.join(process.cwd(), 'debug-screenshots')
if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true })
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const screenshotPath = path.join(debugDir, `goHome-${this.bot.currentAccountEmail}-${timestamp}.png`)
await page.screenshot({ path: screenshotPath, fullPage: true })
this.bot.log(this.bot.isMobile, 'GO-HOME', `Debug screenshot saved: ${screenshotPath}`, 'warn')
} catch (error) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Screenshot capture failed: ${error}`, 'warn')
}
}
// Not suspended, just activities not loaded yet - continue to next iteration
this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn')
// Below runs if the homepage was unable to be visited
const currentURL = new URL(page.url())
if (currentURL.hostname !== dashboardURL.hostname) {
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(1000)
await page.goto(this.bot.config.baseURL)
// IMPROVED: Wait for page ready after redirect
// FIXED: Use timeoutMs parameter with increased timeout
await waitForPageReady(page, {
timeoutMs: 15000, // FIXED: 15s timeout to handle slower network conditions
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log')
})
} else {
// FIXED: We're on the right URL but activities not found - force page reload to trigger DOM re-render
// This fixes the issue where Tyler needs to manually refresh to see activities
this.bot.log(this.bot.isMobile, 'GO-HOME', 'On correct URL but activities missing - forcing page reload to trigger DOM render', 'warn')
try {
// IMPROVED: Try alternative reload strategies based on iteration
if (iteration === 1) {
// First attempt: Simple reload
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 })
} else if (iteration === 2) {
// Second attempt: Navigate to full dashboard URL (not just base)
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Trying full dashboard URL: /rewards/dashboard', 'log')
await page.goto(`${this.bot.config.baseURL}/rewards/dashboard`, { waitUntil: 'domcontentloaded', timeout: 15000 })
} else if (iteration === 3) {
// Third attempt: Clear localStorage and reload
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Clearing localStorage and reloading', 'log')
await page.evaluate(() => localStorage.clear())
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 })
} else {
// Final attempts: Hard reload with cache bypass
await page.reload({ waitUntil: 'networkidle', timeout: 20000 })
}
await waitForPageReady(page, {
timeoutMs: 10000,
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GO-HOME', msg, 'log')
})
// Try scrolling to force lazy-loaded elements to render
await page.evaluate(() => {
window.scrollTo(0, 200)
window.scrollTo(0, 0)
})
await this.bot.utils.wait(1000)
} catch (reloadError) {
const reloadMsg = reloadError instanceof Error ? reloadError.message : String(reloadError)
this.bot.log(this.bot.isMobile, 'GO-HOME', `Page reload failed: ${reloadMsg}`, 'warn')
}
}
await this.bot.utils.wait(2000)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GO-HOME', `[goHome] Navigation failed: ${errorMessage}`, 'error')
throw error
}
}
/**
* Fetch user dashboard data
* @returns {DashboardData} Object of user bing rewards dashboard data
*/
async getDashboardData(page?: Page): Promise<DashboardData> {
const target = page ?? this.bot.homePage
const dashboardURL = new URL(this.bot.config.baseURL)
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(target)
}
// Reload with retry
await this.reloadPageWithRetry(target, 2)
// IMPROVED: Smart wait for activities element
const activitiesResult = await waitForElementSmart(target, SELECTORS.MORE_ACTIVITIES, {
initialTimeoutMs: 3000,
extendedTimeoutMs: 7000,
state: 'attached',
logFn: (msg) => this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', msg, 'log')
})
if (!activitiesResult.found) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Activities element not found after ${activitiesResult.timeMs}ms, attempting to proceed anyway`, 'warn')
}
let scriptContent = await this.extractDashboardScript(target)
if (!scriptContent) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn')
// Force a navigation retry once before failing hard
await this.goHome(target)
// IMPROVED: Smart page readiness check instead of fixed wait
// FIXED: Use timeoutMs parameter with increased timeout
await waitForPageReady(target, {
timeoutMs: 15000, // FIXED: 15s timeout for dashboard recovery
logFn: (msg) => this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', msg, 'log')
}).catch((error) => {
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load incomplete: ${errorMsg}`, 'warn')
})
scriptContent = await this.extractDashboardScript(target)
if (!scriptContent) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
throw new Error('Dashboard data not found within script - check page structure')
}
}
// Extract the dashboard object from the script content
const dashboardData = await this.parseDashboardFromScript(target, scriptContent)
if (!dashboardData) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
throw new Error('Unable to parse dashboard script - inspect recent logs and page markup')
}
return dashboardData
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `[getDashboardData] Failed to fetch dashboard data: ${errorMessage}`, 'error')
throw error
}
}
/**
* Reload page with retry logic
* FIXED: Added global timeout to prevent infinite retry loops
*/
private async reloadPageWithRetry(page: Page, maxAttempts: number): Promise<void> {
const startTime = Date.now()
const MAX_TOTAL_TIME_MS = 30000 // 30 seconds max total
let lastError: unknown = null
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
// Check global timeout
if (Date.now() - startTime > MAX_TOTAL_TIME_MS) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload retry exceeded total timeout (${MAX_TOTAL_TIME_MS}ms)`, 'warn')
break
}
try {
await page.reload({ waitUntil: 'domcontentloaded' })
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
lastError = null
break
} catch (re) {
lastError = re
const msg = (re instanceof Error ? re.message : String(re))
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload failed attempt ${attempt}: ${msg}`, 'warn')
if (msg.includes('has been closed')) {
if (attempt === 1) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
try { await this.goHome(page) } catch { /* Final recovery attempt - failure is acceptable */ }
} else {
break
}
}
if (attempt === maxAttempts) {
await this.bot.utils.wait(1000)
}
}
}
if (lastError) throw lastError
}
/**
* Extract dashboard script from page
*/
private async extractDashboardScript(page: Page): Promise<string | null> {
return await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
const dashboardPatterns = ['var dashboard', 'dashboard=', 'dashboard :']
const targetScript = scripts.find(script => {
const text = script.innerText
return text && dashboardPatterns.some(pattern => text.includes(pattern))
})
return targetScript?.innerText || null
})
}
/**
* Parse dashboard object from script content
* IMPROVED: Enhanced validation with structure checks
*/
private async parseDashboardFromScript(page: Page, scriptContent: string): Promise<DashboardData | null> {
return await page.evaluate((scriptContent: string) => {
const patterns = [
/var\s+dashboard\s*=\s*(\{[\s\S]*?\});/,
/dashboard\s*=\s*(\{[\s\S]*?\});/,
/var\s+dashboard\s*:\s*(\{[\s\S]*?\})\s*[,;]/
]
for (const regex of patterns) {
const match = regex.exec(scriptContent)
if (match && match[1]) {
try {
const jsonStr = match[1]
// Validate basic JSON structure before parsing
const trimmed = jsonStr.trim()
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
continue
}
const parsed = JSON.parse(jsonStr)
// Enhanced validation: check structure and type
if (typeof parsed !== 'object' || parsed === null) {
continue
}
// Validate essential dashboard properties exist
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
continue
}
// Successfully validated dashboard structure
return parsed
} catch (e) {
// JSON.parse failed or validation error - try next pattern
continue
}
}
}
return null
}, scriptContent)
}
/**
* Get search point counters
* @returns {Counters} Object of search counter data
*/
async getSearchPoints(): Promise<Counters> {
const dashboardData = await this.getDashboardData() // Always fetch newest data
return dashboardData.userStatus.counters
}
/**
* Get total earnable points with web browser
* @returns {number} Total earnable points
*/
async getBrowserEarnablePoints(): Promise<EarnablePoints> {
try {
let desktopSearchPoints = 0
let mobileSearchPoints = 0
let dailySetPoints = 0
let morePromotionsPoints = 0
const data = await this.getDashboardData()
// Desktop Search Points
if (data.userStatus.counters.pcSearch?.length) {
data.userStatus.counters.pcSearch.forEach(x => desktopSearchPoints += (x.pointProgressMax - x.pointProgress))
}
// Mobile Search Points
if (data.userStatus.counters.mobileSearch?.length) {
data.userStatus.counters.mobileSearch.forEach(x => mobileSearchPoints += (x.pointProgressMax - x.pointProgress))
}
// Daily Set
data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => dailySetPoints += (x.pointProgressMax - x.pointProgress))
// More Promotions
if (data.morePromotions?.length) {
data.morePromotions.forEach(x => {
// Only count points from supported activities
if (['quiz', 'urlreward'].includes(x.promotionType) && x.exclusiveLockedFeatureStatus !== 'locked') {
morePromotionsPoints += (x.pointProgressMax - x.pointProgress)
}
})
}
const totalEarnablePoints = desktopSearchPoints + mobileSearchPoints + dailySetPoints + morePromotionsPoints
return {
dailySetPoints,
morePromotionsPoints,
desktopSearchPoints,
mobileSearchPoints,
totalEarnablePoints
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-BROWSER-EARNABLE-POINTS', `[getBrowserEarnablePoints] Failed to calculate earnable points: ${errorMessage}`, 'error')
throw error
}
}
/**
* Get total earnable points with mobile app
* @returns {number} Total earnable points
*/
async getAppEarnablePoints(accessToken: string) {
try {
const points = {
readToEarn: 0,
checkIn: 0,
totalEarnablePoints: 0
}
const eligibleOffers = [
'ENUS_readarticle3_30points',
'Gamification_Sapphire_DailyCheckIn'
]
const data = await this.getDashboardData()
// 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: URLS.APP_USER_DATA,
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
}
}
const userDataResponse: AppUserData = (await this.bot.axios.request(userDataRequest)).data
const userData = userDataResponse.response
const eligibleActivities = userData.promotions.filter((x) => eligibleOffers.includes(x.attributes.offerid ?? ''))
for (const item of eligibleActivities) {
if (item.attributes.type === 'msnreadearn') {
points.readToEarn = parseInt(item.attributes.pointmax ?? '', 10) - parseInt(item.attributes.pointprogress ?? '', 10)
break
} else if (item.attributes.type === 'checkin') {
const checkInDay = parseInt(item.attributes.progress ?? '', 10) % 7
const today = new Date()
const lastUpdated = new Date(item.attributes.last_updated ?? '')
if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) {
points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '', 10)
}
break
}
}
points.totalEarnablePoints = points.readToEarn + points.checkIn
return points
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-APP-EARNABLE-POINTS', `[getAppEarnablePoints] Failed to fetch app earnable points: ${errorMessage}`, 'error')
throw error
}
}
/**
* Get current point amount
* @returns {number} Current total point amount
*/
async getCurrentPoints(): Promise<number> {
try {
const data = await this.getDashboardData()
return data.userStatus.availablePoints
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-CURRENT-POINTS', `[getCurrentPoints] Failed to fetch current points: ${errorMessage}`, 'error')
throw error
}
}
/**
* Parse quiz data from provided page
* @param {Page} page Playwright page
* @returns {QuizData} Quiz data object
*/
async getQuizData(page: Page): Promise<QuizData> {
try {
// Wait for page to be fully loaded
await page.waitForLoadState('domcontentloaded')
await this.bot.utils.wait(TIMEOUTS.MEDIUM)
const html = await page.content()
const $ = load(html)
// Try multiple possible variable names
const possibleVariables = [
'_w.rewardsQuizRenderInfo',
'rewardsQuizRenderInfo',
'_w.quizRenderInfo',
'quizRenderInfo'
]
let scriptContent = ''
let foundVariable = ''
for (const varName of possibleVariables) {
scriptContent = $('script')
.toArray()
.map(el => $(el).text())
.find(t => t.includes(varName)) || ''
if (scriptContent) {
foundVariable = varName
break
}
}
if (scriptContent && foundVariable) {
// Escape dots in variable name for regex
const escapedVar = foundVariable.replace(/\./g, '\\.')
const regex = new RegExp(`${escapedVar}\\s*=\\s*({.*?});`, 's')
const match = regex.exec(scriptContent)
if (match && match[1]) {
const quizData = JSON.parse(match[1])
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found quiz data using variable: ${foundVariable}`, 'log')
return quizData
} else {
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Variable ${foundVariable} found but could not extract JSON data`, 'error')
throw new Error(`Quiz data variable ${foundVariable} found but JSON extraction failed`)
}
} else {
// Log available scripts for debugging
const allScripts = $('script')
.toArray()
.map(el => $(el).text())
.filter(t => t.length > 0)
.map(t => t.substring(0, 100))
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error')
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn')
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
throw new Error('Script containing quiz data not found - check page structure')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `[getQuizData] Failed to extract quiz data: ${errorMessage}`, 'error')
throw error
}
}
async waitForQuizRefresh(page: Page): Promise<boolean> {
try {
// IMPROVED: Smart wait replaces fixed 10s timeout with adaptive 2s+5s detection
const result = await waitForElementSmart(page, SELECTORS.QUIZ_CREDITS, {
initialTimeoutMs: 2000,
extendedTimeoutMs: TIMEOUTS.DASHBOARD_WAIT - 2000,
state: 'visible',
logFn: (msg) => this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', msg)
})
if (!result.found) {
this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'Quiz credits element not found', 'error')
return false
}
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
return true
} catch (error) {
this.bot.log(this.bot.isMobile, 'QUIZ-REFRESH', 'An error occurred:' + error, 'error')
return false
}
}
async checkQuizCompleted(page: Page): Promise<boolean> {
try {
// IMPROVED: Smart wait replaces fixed 2s timeout with adaptive detection
const result = await waitForElementSmart(page, SELECTORS.QUIZ_COMPLETE, {
initialTimeoutMs: 1000,
extendedTimeoutMs: TIMEOUTS.MEDIUM_LONG,
state: 'visible',
logFn: (msg) => this.bot.log(this.bot.isMobile, 'QUIZ-COMPLETE', msg)
})
if (!result.found) {
return false
}
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
return true
} catch (error) {
return false
}
}
async loadInCheerio(page: Page): Promise<CheerioAPI> {
const html = await page.content()
const $ = load(html)
return $
}
async getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise<string> {
let selector = ''
try {
const html = await page.content()
const $ = load(html)
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}"]`
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error')
}
return selector
}
async closeBrowser(browser: BrowserContext, email: string) {
try {
// Save cookies
await saveSessionData(this.bot.config.sessionPath, browser, email, this.bot.isMobile)
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
// Close browser
await browser.close()
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.log(this.bot.isMobile, 'CLOSE-BROWSER', `[closeBrowser] Failed to close browser cleanly: ${errorMessage}`, 'error')
throw error
}
}
}