import { Page } from 'playwright' import { CheerioAPI, load } from 'cheerio' import axios, { AxiosRequestConfig } from 'axios' import { MicrosoftRewardsBot } from '../index' import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData' import { QuizData } from './../interface/QuizData' import { AppUserData } from '../interface/AppUserData' 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 with web browser * @returns {number} Total earnable points */ async getBrowserEarnablePoints(): 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.exclusiveLockedFeatureStatus !== 'locked') { totalEarnablePoints += (x.pointProgressMax - x.pointProgress) } }) } return totalEarnablePoints } catch (error) { throw this.bot.log('GET-BROWSER-EARNABLE-POINTS', 'An error occurred:' + error, 'error') } } /** * Get total earnable points with mobile app * @returns {number} Total earnable points */ async getAppEarnablePoints(accessToken: string): Promise { try { const eligibleOffers = [ 'ENUS_readarticle3_30points', 'Gamification_Sapphire_DailyCheckIn' ] let totalEarnablePoints = 0 const data = await this.getDashboardData() let geoLocale = data.userProfile.attributes.country geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us' const userDataRequest: AxiosRequestConfig = { url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613', method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-Rewards-Country': geoLocale, 'X-Rewards-Language': 'en' } } const userDataResponse: AppUserData = (await axios(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') { totalEarnablePoints += parseInt(item.attributes.pointmax ?? '') - parseInt(item.attributes.pointprogress ?? '') break } else if (item.attributes.type === 'checkin') { const checkInDay = parseInt(item.attributes.progress ?? '') % 7 if (checkInDay < 6 && (new Date()).getDate() != (new Date(item.attributes.last_updated ?? '')).getDate()) { totalEarnablePoints += parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '') } break } } return totalEarnablePoints } catch (error) { throw this.bot.log('GET-APP-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 } }