import { Page } from 'playwright' import { CheerioAPI, load } from 'cheerio' import { MicrosoftRewardsBot } from '../index' import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData' import { QuizData } from './../interface/QuizData' export default class BrowserFunc { private bot: MicrosoftRewardsBot constructor(bot: MicrosoftRewardsBot) { this.bot = bot } /** * 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) const maxIterations = 5 // Maximum iterations set to 5 for (let iteration = 1; iteration <= maxIterations; iteration++) { await this.bot.utils.wait(3000) await this.bot.browser.utils.tryDismissCookieBanner(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('GO-HOME', 'This account is suspended!', 'error') throw new Error('Account has been suspended!') } try { // If activities are found, exit the loop await page.waitForSelector('#more-activities', { timeout: 1000 }) this.bot.log('GO-HOME', 'Visited homepage successfully') break } catch (error) { // Continue if element is not found } // 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(2000) await page.goto(this.bot.config.baseURL) } else { this.bot.log('GO-HOME', 'Visited homepage successfully') break } await this.bot.utils.wait(5000) } } catch (error) { throw this.bot.log('GO-HOME', 'An error occurred:' + error, 'error') } } /** * Fetch user dashboard data * @returns {DashboardData} Object of user bing rewards dashboard data */ async getDashboardData(): Promise { const dashboardURL = new URL(this.bot.config.baseURL) const currentURL = new URL(this.bot.homePage.url()) // Should never happen since tasks are opened in a new tab! if (currentURL.hostname !== dashboardURL.hostname) { this.bot.log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page') await this.goHome(this.bot.homePage) } // Reload the page to get new data await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' }) const scriptContent = await this.bot.homePage.evaluate(() => { const scripts = Array.from(document.querySelectorAll('script')) const targetScript = scripts.find(script => script.innerText.includes('var dashboard')) return targetScript?.innerText ? targetScript.innerText : null }) if (!scriptContent) { throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error') } // Extract the dashboard object from the script content const dashboardData = await this.bot.homePage.evaluate(scriptContent => { // Extract the dashboard object using regex const regex = /var dashboard = (\{.*?\});/s const match = regex.exec(scriptContent) if (match && match[1]) { return JSON.parse(match[1]) } }, scriptContent) if (!dashboardData) { throw this.bot.log('GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error') } return dashboardData } /** * Get search point counters * @returns {Counters} Object of search counter data */ async getSearchPoints(): Promise { const dashboardData = await this.getDashboardData() // Always fetch newest data return dashboardData.userStatus.counters } /** * Get total earnable points * @returns {number} Total earnable points */ async getEarnablePoints(): Promise { try { const data = await this.getDashboardData() // These only include the points from tasks that the script can complete! let totalEarnablePoints = 0 // Desktop Search Points if (data.userStatus.counters.pcSearch?.length) { data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) } // Mobile Search Points if (data.userStatus.counters.mobileSearch?.length) { data.userStatus.counters.mobileSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) } // Daily Set data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => totalEarnablePoints += (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.attributes.is_unlocked) { totalEarnablePoints += (x.pointProgressMax - x.pointProgress) } }) } return totalEarnablePoints } catch (error) { throw this.bot.log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error') } } /** * Get current point amount * @returns {number} Current total point amount */ async getCurrentPoints(): Promise { try { const data = await this.getDashboardData() return data.userStatus.availablePoints } catch (error) { throw this.bot.log('GET-CURRENT-POINTS', 'An error occurred:' + error, 'error') } } /** * Parse quiz data from provided page * @param {Page} page Playwright page * @returns {QuizData} Quiz data object */ async getQuizData(page: Page): Promise { try { const html = await page.content() const $ = load(html) const scriptContent = $('script').filter((index, element) => { return $(element).text().includes('_w.rewardsQuizRenderInfo') }).text() if (scriptContent) { const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s const match = regex.exec(scriptContent) if (match && match[1]) { const quizData = JSON.parse(match[1]) return quizData } else { throw this.bot.log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error') } } else { throw this.bot.log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error') } } catch (error) { throw this.bot.log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error') } } async waitForQuizRefresh(page: Page): Promise { try { await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 }) await this.bot.utils.wait(2000) return true } catch (error) { this.bot.log('QUIZ-REFRESH', 'An error occurred:' + error, 'error') return false } } async checkQuizCompleted(page: Page): Promise { try { await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 }) await this.bot.utils.wait(2000) return true } catch (error) { return false } } async refreshCheerio(page: Page): Promise { const html = await page.content() const $ = load(html) return $ } async getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise { let selector = '' try { const html = await page.content() const $ = load(html) const element = $('.offer-cta').toArray().find(x => x.attribs.href?.includes(activity.offerId)) if (element) { selector = `a[href*="${element.attribs.href}"]` } } catch (error) { this.bot.log('GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error') } return selector } }