From c2b68faa7441af7351e0e0c7530fea085ae910ee Mon Sep 17 00:00:00 2001 From: TheNetsky <56271887+TheNetsky@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:31:35 +0200 Subject: [PATCH] 1.2.2 --- package.json | 6 +- src/browser/Browser.ts | 15 +- src/browser/BrowserFunc.ts | 406 ++++++++++---------- src/browser/BrowserUtil.ts | 127 ++++--- src/config.json | 8 +- src/functions/Activities.ts | 52 +++ src/functions/Login.ts | 200 +++++----- src/functions/Workers.ts | 281 +++++++------- src/functions/activities/ABC.ts | 78 ++-- src/functions/activities/Poll.ts | 35 +- src/functions/activities/Quiz.ts | 129 ++++--- src/functions/activities/Search.ts | 500 ++++++++++++------------- src/functions/activities/ThisOrThat.ts | 49 ++- src/functions/activities/UrlReward.ts | 27 +- src/index.ts | 91 +++-- src/interface/Config.ts | 35 ++ src/util/{Account.ts => Load.ts} | 17 +- src/util/Utils.ts | 72 ++-- src/util/Webhook.ts | 7 +- tsconfig.json | 3 +- 20 files changed, 1150 insertions(+), 988 deletions(-) create mode 100644 src/functions/Activities.ts create mode 100644 src/interface/Config.ts rename src/util/{Account.ts => Load.ts} (55%) diff --git a/package.json b/package.json index 745dd00..b436390 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microsoft-rewards-script", - "version": "1.2.1", + "version": "1.2.2", "description": "Automatically do tasks for Microsoft Rewards but in TS!", "main": "index.js", "engines": { @@ -32,9 +32,9 @@ "cheerio": "^1.0.0-rc.12", "eslint": "^8.49.0", "eslint-plugin-modules-newline": "^0.0.6", - "puppeteer": "^21.2.1", + "puppeteer": "^21.4.1", "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "ts-node": "^10.9.1" } -} \ No newline at end of file +} diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index 00a80d0..43cf742 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -1,23 +1,28 @@ import puppeteer from 'puppeteer-extra' import stealthPlugin from 'puppeteer-extra-plugin-stealth' +import { MicrosoftRewardsBot } from '../index' + import { getUserAgent } from '../util/UserAgent' -import { loadSesion } from './BrowserFunc' import { AccountProxy } from '../interface/Account' -import { headless } from '../config.json' - puppeteer.use(stealthPlugin()) + class Browser { + private bot: MicrosoftRewardsBot + + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot + } async createBrowser(email: string, proxy: AccountProxy, isMobile: boolean) { const userAgent = await getUserAgent(isMobile) const browser = await puppeteer.launch({ - headless: headless, - userDataDir: await loadSesion(email), + headless: this.bot.config.headless, + userDataDir: await this.bot.browser.func.loadSesion(email), args: [ '--no-sandbox', '--mute-audio', diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index ccc6e86..a8a7712 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -1,248 +1,254 @@ import { Page } from 'puppeteer' +import { CheerioAPI, load } from 'cheerio' import fs from 'fs' import path from 'path' -import { CheerioAPI, load } from 'cheerio' -import { tryDismissAllMessages, tryDismissCookieBanner } from './BrowserUtil' -import { getFormattedDate, wait } from './../util/Utils' -import { log } from './../util/Logger' +import { MicrosoftRewardsBot } from '../index' import { Counters, DashboardData, MorePromotion, PromotionalItem } from './../interface/DashboardData' import { QuizData } from './../interface/QuizData' -import { baseURL, sessionPath } from './../config.json' -export async function goHome(page: Page): Promise { +export default class BrowserFunc { + private bot: MicrosoftRewardsBot - try { - const dashboardURL = new URL(baseURL) - - await page.goto(baseURL) - - const maxIterations = 5 // Maximum iterations set to 5 - - for (let iteration = 1; iteration <= maxIterations; iteration++) { - await wait(3000) - await tryDismissCookieBanner(page) - - // Check if account is suspended - const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) - if (isSuspended) { - 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 }) - break - - } catch (error) { - // Continue if element is not found - } - - const currentURL = new URL(page.url()) - - if (currentURL.hostname !== dashboardURL.hostname) { - await tryDismissAllMessages(page) - - await wait(2000) - await page.goto(baseURL) - } - - await wait(5000) - log('GO-HOME', 'Visited homepage successfully') - } - - } catch (error) { - console.error('An error occurred:', error) - return false + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot } - return true -} + async goHome(page: Page): Promise { -export async function getDashboardData(page: Page): Promise { - const dashboardURL = new URL(baseURL) - const currentURL = new URL(page.url()) + try { + const dashboardURL = new URL(this.bot.config.baseURL) - // Should never happen since tasks are opened in a new tab! - if (currentURL.hostname !== dashboardURL.hostname) { - log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page') - await goHome(page) + 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', { visible: true, timeout: 3000 }).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 }) + break + + } catch (error) { + // Continue if element is not found + } + + 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) + } + + await this.bot.utils.wait(5000) + this.bot.log('GO-HOME', 'Visited homepage successfully') + } + + } catch (error) { + console.error('An error occurred:', error) + return false + } + + return true } - // Reload the page to get new data - await page.reload({ waitUntil: 'networkidle2' }) + async getDashboardData(page: Page): Promise { + const dashboardURL = new URL(this.bot.config.baseURL) + const currentURL = new URL(page.url()) - const scriptContent = await page.evaluate(() => { - const scripts = Array.from(document.querySelectorAll('script')) - const targetScript = scripts.find(script => script.innerText.includes('var dashboard')) - - if (targetScript) { - return targetScript.innerText - } else { - throw log('GET-DASHBOARD-DATA', 'Script containing dashboard data not found', 'error') + // 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(page) } - }) - // Extract the dashboard object from the script content - const dashboardData = await page.evaluate(scriptContent => { - // Extract the dashboard object using regex - const regex = /var dashboard = (\{.*?\});/s - const match = regex.exec(scriptContent) + // Reload the page to get new data + await page.reload({ waitUntil: 'networkidle2' }) - if (match && match[1]) { - return JSON.parse(match[1]) - } else { - throw log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error') - } - }, scriptContent) + const scriptContent = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script')) + const targetScript = scripts.find(script => script.innerText.includes('var dashboard')) - return dashboardData -} - -export async function 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 + if (targetScript) { + return targetScript.innerText } else { - throw log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error') - } - } else { - throw log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error') - } - - } catch (error) { - throw log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error') - } - -} - -export async function getSearchPoints(page: Page): Promise { - const dashboardData = await getDashboardData(page) // Always fetch newest data - - return dashboardData.userStatus.counters -} - -export async function getEarnablePoints(data: DashboardData, page: null | Page = null): Promise { - try { - // Fetch new data if page is provided - if (page) { - data = await getDashboardData(page) - } - - // These only include the points from tasks that the script can complete! - let totalEarnablePoints = 0 - - // Desktop Search Points - 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[getFormattedDate()]?.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress)) - - // More Promotions - data.morePromotions.forEach(x => { - // Only count points from supported activities - if (['quiz', 'urlreward'].includes(x.activityType)) { - totalEarnablePoints += (x.pointProgressMax - x.pointProgress) + throw this.bot.log('GET-DASHBOARD-DATA', 'Script containing dashboard data not found', 'error') } }) - return totalEarnablePoints - } catch (error) { - throw log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error') - } -} + // Extract the dashboard object from the script content + const dashboardData = await page.evaluate(scriptContent => { + // Extract the dashboard object using regex + const regex = /var dashboard = (\{.*?\});/s + const match = regex.exec(scriptContent) -export async function getCurrentPoints(data: DashboardData, page: null | Page = null): Promise { - try { - // Fetch new data if page is provided - if (page) { - data = await getDashboardData(page) + if (match && match[1]) { + return JSON.parse(match[1]) + } else { + throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error') + } + }, scriptContent) + + return dashboardData + } + + 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') } - return data.userStatus.availablePoints - } catch (error) { - throw log('GET-CURRENT-POINTS', 'An error occurred:' + error, 'error') } -} -export async function loadSesion(email: string): Promise { - const sessionDir = path.join(__dirname, sessionPath, email) + async getSearchPoints(page: Page): Promise { + const dashboardData = await this.getDashboardData(page) // Always fetch newest data - try { - // Create session dir - if (!fs.existsSync(sessionDir)) { - await fs.promises.mkdir(sessionDir, { recursive: true }) + return dashboardData.userStatus.counters + } + + async getEarnablePoints(data: DashboardData, page: null | Page = null): Promise { + try { + // Fetch new data if page is provided + if (page) { + data = await this.getDashboardData(page) + } + + // These only include the points from tasks that the script can complete! + let totalEarnablePoints = 0 + + // Desktop Search Points + 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 + data.morePromotions.forEach(x => { + // Only count points from supported activities + if (['quiz', 'urlreward'].includes(x.activityType)) { + totalEarnablePoints += (x.pointProgressMax - x.pointProgress) + } + }) + + return totalEarnablePoints + } catch (error) { + throw this.bot.log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error') } - - return sessionDir - } catch (error) { - throw new Error(error as string) } -} -export async function waitForQuizRefresh(page: Page): Promise { - try { - await page.waitForSelector('#rqHeaderCredits', { visible: true, timeout: 5000 }) - await wait(2000) + async getCurrentPoints(data: DashboardData, page: null | Page = null): Promise { + try { + // Fetch new data if page is provided + if (page) { + data = await this.getDashboardData(page) + } - return true - } catch (error) { - log('QUIZ-REFRESH', 'An error occurred:' + error, 'error') - return false + return data.userStatus.availablePoints + } catch (error) { + throw this.bot.log('GET-CURRENT-POINTS', 'An error occurred:' + error, 'error') + } } -} -export async function checkQuizCompleted(page: Page): Promise { - try { - await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 1000 }) - await wait(2000) + async loadSesion(email: string): Promise { + const sessionDir = path.join(__dirname, this.bot.config.sessionPath, email) - return true - } catch (error) { - return false + try { + // Create session dir + if (!fs.existsSync(sessionDir)) { + await fs.promises.mkdir(sessionDir, { recursive: true }) + } + + return sessionDir + } catch (error) { + throw new Error(error as string) + } } -} -export async function refreshCheerio(page: Page): Promise { - const html = await page.content() - const $ = load(html) + async waitForQuizRefresh(page: Page): Promise { + try { + await page.waitForSelector('#rqHeaderCredits', { visible: true, timeout: 5000 }) + await this.bot.utils.wait(2000) - return $ -} + return true + } catch (error) { + this.bot.log('QUIZ-REFRESH', 'An error occurred:' + error, 'error') + return false + } + } -export async function getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise { - let selector = '' - try { + async checkQuizCompleted(page: Page): Promise { + try { + await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 1000 }) + 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) - const element = $('.offer-cta').toArray().find(x => x.attribs.href?.includes(activity.offerId)) - if (element) { - selector = `a[href*="${element.attribs.href}"]` - } - } catch (error) { - log('GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error') + 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 } - return selector } \ No newline at end of file diff --git a/src/browser/BrowserUtil.ts b/src/browser/BrowserUtil.ts index 3298203..ea13e99 100644 --- a/src/browser/BrowserUtil.ts +++ b/src/browser/BrowserUtil.ts @@ -1,81 +1,90 @@ import { Page } from 'puppeteer' -import { wait } from './../util/Utils' -import { log } from './../util/Logger' +import { MicrosoftRewardsBot } from '../index' -export async function tryDismissAllMessages(page: Page): Promise { - const buttons = [ - { 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' } - ] - let result = false +export default class BrowserUtil { + private bot: MicrosoftRewardsBot - for (const button of buttons) { + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot + } + + async tryDismissAllMessages(page: Page): Promise { + const buttons = [ + { 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' } + ] + + let result = false + + for (const button of buttons) { + try { + const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 }) + if (element) { + await element.click() + result = true + } + + } catch (error) { + continue + } + } + + return result + } + + async tryDismissCookieBanner(page: Page): Promise { try { - const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 }) - if (element) { - await element.click() - result = true + await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 }) + const cookieBanner = await page.$('#cookieConsentContainer') + + if (cookieBanner) { + const button = await cookieBanner.$('button') + if (button) { + await button.click() + await this.bot.utils.wait(2000) + } } } catch (error) { - continue + // Continue if element is not found or other error occurs } } - return result -} + async tryDismissBingCookieBanner(page: Page): Promise { + try { + await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 }) + const cookieBanner = await page.$('#bnp_btn_accept') -export async function tryDismissCookieBanner(page: Page): Promise { - try { - await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 }) - const cookieBanner = await page.$('#cookieConsentContainer') - - if (cookieBanner) { - const button = await cookieBanner.$('button') - if (button) { - await button.click() - await wait(2000) + if (cookieBanner) { + await cookieBanner.click() } + } catch (error) { + // Continue if element is not found or other error occurs } - - } catch (error) { - // Continue if element is not found or other error occurs } -} -export async function tryDismissBingCookieBanner(page: Page): Promise { - try { - await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 }) - const cookieBanner = await page.$('#bnp_btn_accept') + async getLatestTab(page: Page): Promise { + try { + await this.bot.utils.wait(500) - if (cookieBanner) { - await cookieBanner.click() + const browser = page.browser() + const pages = await browser.pages() + const newTab = pages[pages.length - 1] + + if (newTab) { + return newTab + } + + throw this.bot.log('GET-NEW-TAB', 'Unable to get latest tab', 'error') + } catch (error) { + throw this.bot.log('GET-NEW-TAB', 'An error occurred:' + error, 'error') } - } catch (error) { - // Continue if element is not found or other error occurs } -} -export async function getLatestTab(page: Page): Promise { - try { - await wait(500) - - const browser = page.browser() - const pages = await browser.pages() - const newTab = pages[pages.length - 1] - - if (newTab) { - return newTab - } - - throw log('GET-NEW-TAB', 'Unable to get latest tab', 'error') - } catch (error) { - throw log('GET-NEW-TAB', 'An error occurred:' + error, 'error') - } } \ No newline at end of file diff --git a/src/config.json b/src/config.json index e8dd46c..b081fbe 100644 --- a/src/config.json +++ b/src/config.json @@ -2,7 +2,7 @@ "baseURL": "https://rewards.bing.com", "sessionPath": "sessions", "headless": false, - "runOnZeroPoints": true, + "runOnZeroPoints": false, "clusters": 1, "workers": { "doDailySet": true, @@ -14,7 +14,11 @@ "searchSettings": { "useGeoLocaleQueries": false, "scrollRandomResults": true, - "clickRandomResults": true + "clickRandomResults": true, + "searchDelay": { + "min": 10000, + "max": 20000 + } }, "webhook": { "enabled": false, diff --git a/src/functions/Activities.ts b/src/functions/Activities.ts new file mode 100644 index 0000000..71ae7cb --- /dev/null +++ b/src/functions/Activities.ts @@ -0,0 +1,52 @@ +import { Page } from 'puppeteer' + +import { MicrosoftRewardsBot } from '../index' + +import { Search } from './activities/Search' +import { ABC } from './activities/ABC' +import { Poll } from './activities/Poll' +import { Quiz } from './activities/Quiz' +import { ThisOrThat } from './activities/ThisOrThat' +import { UrlReward } from './activities/UrlReward' + +import { DashboardData } from '../interface/DashboardData' + + +export default class Activities { + private bot: MicrosoftRewardsBot + + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot + } + + doSearch = async (page: Page, data: DashboardData, mobile: boolean): Promise => { + const search = new Search(this.bot) + await search.doSearch(page, data, mobile) + } + + doABC = async (page: Page): Promise => { + const abc = new ABC(this.bot) + await abc.doABC(page) + } + + doPoll = async (page: Page): Promise => { + const poll = new Poll(this.bot) + await poll.doPoll(page) + } + + doThisOrThat = async (page: Page): Promise => { + const thisOrThat = new ThisOrThat(this.bot) + await thisOrThat.doThisOrThat(page) + } + + doQuiz = async (page: Page): Promise => { + const quiz = new Quiz(this.bot) + await quiz.doQuiz(page) + } + + doUrlReward = async (page: Page): Promise => { + const urlReward = new UrlReward(this.bot) + await urlReward.doUrlReward(page) + } + +} \ No newline at end of file diff --git a/src/functions/Login.ts b/src/functions/Login.ts index 3d1cfe1..853c91c 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -1,125 +1,133 @@ import { Page } from 'puppeteer' import readline from 'readline' +import { MicrosoftRewardsBot } from '../index' + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) -import { tryDismissAllMessages, tryDismissBingCookieBanner } from '../browser/BrowserUtil' -import { wait } from '../util/Utils' -import { log } from '../util/Logger' -export async function login(page: Page, email: string, password: string) { +export class Login { + private bot: MicrosoftRewardsBot - try { - // Navigate to the Bing login page - await page.goto('https://login.live.com/') + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot + } - const isLoggedIn = await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 5000 }).then(() => true).catch(() => false) + async login(page: Page, email: string, password: string) { - if (!isLoggedIn) { - const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 5000 }).then(() => true).catch(() => false) - if (isLocked) { - log('LOGIN', 'This account has been locked!', 'error') - throw new Error('Account has been locked!') + try { + // Navigate to the Bing login page + await page.goto('https://login.live.com/') + + const isLoggedIn = await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 5000 }).then(() => true).catch(() => false) + + if (!isLoggedIn) { + const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 5000 }).then(() => true).catch(() => false) + if (isLocked) { + this.bot.log('LOGIN', 'This account has been locked!', 'error') + throw new Error('Account has been locked!') + } + + await page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 }) + + await this.execLogin(page, email, password) + this.bot.log('LOGIN', 'Logged into Microsoft successfully') + } else { + this.bot.log('LOGIN', 'Already logged in') } - await page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 }) + // Check if logged in to bing + await this.checkBingLogin(page) - await execLogin(page, email, password) - log('LOGIN', 'Logged into Microsoft successfully') - } else { - log('LOGIN', 'Already logged in') + // We're done logging in + this.bot.log('LOGIN', 'Logged in successfully') + + } catch (error) { + // Throw and don't continue + throw this.bot.log('LOGIN', 'An error occurred:' + error, 'error') } - - // Check if logged in to bing - await checkBingLogin(page) - - // We're done logging in - log('LOGIN', 'Logged in successfully') - - } catch (error) { - // Throw and don't continue - throw log('LOGIN', 'An error occurred:' + error, 'error') } -} -async function execLogin(page: Page, email: string, password: string) { - await page.type('#i0116', email) - await page.click('#idSIButton9') - - log('LOGIN', 'Email entered successfully') - - try { - await page.waitForSelector('#i0118', { visible: true, timeout: 2000 }) - await wait(2000) - - await page.type('#i0118', password) + private async execLogin(page: Page, email: string, password: string) { + await page.type('#i0116', email) await page.click('#idSIButton9') - } catch (error) { - log('LOGIN', '2FA code required') + this.bot.log('LOGIN', 'Email entered successfully') - const code = await new Promise((resolve) => { - rl.question('Enter 2FA code:\n', (input) => { - rl.close() - resolve(input) + try { + await page.waitForSelector('#i0118', { visible: true, timeout: 2000 }) + await this.bot.utils.wait(2000) + + await page.type('#i0118', password) + await page.click('#idSIButton9') + + } catch (error) { + this.bot.log('LOGIN', '2FA code required') + + const code = await new Promise((resolve) => { + rl.question('Enter 2FA code:\n', (input) => { + rl.close() + resolve(input) + }) }) - }) - await page.type('input[name="otc"]', code) - await page.keyboard.press('Enter') + await page.type('input[name="otc"]', code) + await page.keyboard.press('Enter') - } finally { - log('LOGIN', 'Password entered successfully') - } - - const currentURL = new URL(page.url()) - - while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') { - await tryDismissAllMessages(page) - currentURL.href = page.url() - } - - // Wait for login to complete - await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 }) -} - -async function checkBingLogin(page: Page): Promise { - try { - log('LOGIN-BING', 'Verifying Bing login') - await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F') - - const maxIterations = 5 - - for (let iteration = 1; iteration <= maxIterations; iteration++) { - const currentUrl = new URL(page.url()) - - if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') { - await wait(3000) - await tryDismissBingCookieBanner(page) - - const loggedIn = await checkBingLoginStatus(page) - if (loggedIn) { - log('LOGIN-BING', 'Bing login verification passed!') - break - } - } - - await wait(1000) + } finally { + this.bot.log('LOGIN', 'Password entered successfully') } - } catch (error) { - log('LOGIN-BING', 'An error occurred:' + error, 'error') - } -} + const currentURL = new URL(page.url()) -async function checkBingLoginStatus(page: Page): Promise { - try { - await page.waitForSelector('#id_n', { timeout: 5000 }) - return true - } catch (error) { - return false + while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') { + await this.bot.browser.utils.tryDismissAllMessages(page) + currentURL.href = page.url() + } + + // Wait for login to complete + await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 }) } + + private async checkBingLogin(page: Page): Promise { + try { + this.bot.log('LOGIN-BING', 'Verifying Bing login') + await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F') + + const maxIterations = 5 + + for (let iteration = 1; iteration <= maxIterations; iteration++) { + const currentUrl = new URL(page.url()) + + if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') { + await this.bot.utils.wait(3000) + await this.bot.browser.utils.tryDismissBingCookieBanner(page) + + const loggedIn = await this.checkBingLoginStatus(page) + if (loggedIn) { + this.bot.log('LOGIN-BING', 'Bing login verification passed!') + break + } + } + + await this.bot.utils.wait(1000) + } + + } catch (error) { + this.bot.log('LOGIN-BING', 'An error occurred:' + error, 'error') + } + } + + private async checkBingLoginStatus(page: Page): Promise { + try { + await page.waitForSelector('#id_n', { timeout: 5000 }) + return true + } catch (error) { + return false + } + } + } \ No newline at end of file diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index 52c9dea..f9f3bb9 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -1,173 +1,176 @@ import { Page } from 'puppeteer' -import { doPoll } from './activities/Poll' -import { doQuiz } from './activities/Quiz' -import { doUrlReward } from './activities/UrlReward' -import { doThisOrThat } from './activities/ThisOrThat' -import { doABC } from './activities/ABC' - -import { getPunchCardActivity } from '../browser/BrowserFunc' -import { getLatestTab } from '../browser/BrowserUtil' - -import { getFormattedDate, wait } from '../util/Utils' -import { log } from '../util/Logger' - import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData' -import { baseURL } from '../config.json' +import { MicrosoftRewardsBot } from '../index' -// Daily Set -export async function doDailySet(page: Page, data: DashboardData) { +export class Workers { + public bot: MicrosoftRewardsBot - const todayData = data.dailySetPromotions[getFormattedDate()] - - const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] - - if (!activitiesUncompleted.length) { - log('DAILY-SET', 'All Daily Set" items have already been completed') - return + constructor(bot: MicrosoftRewardsBot) { + this.bot = bot } - // Solve Activities - log('DAILY-SET', 'Started solving "Daily Set" items') + // Daily Set + async doDailySet(page: Page, data: DashboardData) { + const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()] - await solveActivities(page, activitiesUncompleted) + const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] - log('DAILY-SET', 'All "Daily Set" items have been completed') -} - -// Punch Card -export async function doPunchCard(page: Page, data: DashboardData) { - - const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards - - if (!punchCardsUncompleted.length) { - log('PUNCH-CARD', 'All "Punch Cards" have already been completed') - return - } - - for (const punchCard of punchCardsUncompleted) { - const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities + if (!activitiesUncompleted.length) { + this.bot.log('DAILY-SET', 'All Daily Set" items have already been completed') + return + } // Solve Activities - log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`) + this.bot.log('DAILY-SET', 'Started solving "Daily Set" items') - const browser = page.browser() - page = await browser.newPage() + await this.solveActivities(page, activitiesUncompleted) - // Got to punch card index page in a new tab - await page.goto(punchCard.parentPromotion.destinationUrl, { referer: baseURL }) - - await solveActivities(page, activitiesUncompleted, punchCard) - - // Close the punch card index page - await page.close() - - log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`) + this.bot.log('DAILY-SET', 'All "Daily Set" items have been completed') } - log('PUNCH-CARD', 'All "Punch Card" items have been completed') -} + // Punch Card + async doPunchCard(page: Page, data: DashboardData) { -// More Promotions -export async function doMorePromotions(page: Page, data: DashboardData) { - const morePromotions = data.morePromotions + const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards - // Check if there is a promotional item - if (data.promotionalItem) { // Convert and add the promotional item to the array - morePromotions.push(data.promotionalItem as unknown as MorePromotion) + if (!punchCardsUncompleted.length) { + this.bot.log('PUNCH-CARD', 'All "Punch Cards" have already been completed') + return + } + + for (const punchCard of punchCardsUncompleted) { + const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities + + // Solve Activities + this.bot.log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`) + + const browser = page.browser() + page = await browser.newPage() + + // Got to punch card index page in a new tab + await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL }) + + await this.solveActivities(page, activitiesUncompleted, punchCard) + + // Close the punch card index page + await page.close() + + this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`) + } + + this.bot.log('PUNCH-CARD', 'All "Punch Card" items have been completed') } - const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] + // More Promotions + async doMorePromotions(page: Page, data: DashboardData) { + const morePromotions = data.morePromotions - if (!activitiesUncompleted.length) { - log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') - return + // Check if there is a promotional item + if (data.promotionalItem) { // Convert and add the promotional item to the array + morePromotions.push(data.promotionalItem as unknown as MorePromotion) + } + + const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] + + if (!activitiesUncompleted.length) { + this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') + return + } + + // Solve Activities + this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" item') + + await this.solveActivities(page, activitiesUncompleted) + + this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed') } - // Solve Activities - log('MORE-PROMOTIONS', 'Started solving "More Promotions" item') + // Solve all the different types of activities + private async solveActivities(page: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) { + for (const activity of activities) { + try { - await solveActivities(page, activitiesUncompleted) + let selector = `[data-bi-id="${activity.offerId}"]` - log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed') -} + if (punchCard) { + selector = await this.bot.browser.func.getPunchCardActivity(page, activity) -// Solve all the different types of activities -async function solveActivities(page: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) { - for (const activity of activities) { - try { + } else if (activity.name.toLowerCase().includes('membercenter')) { - let selector = `[data-bi-id="${activity.offerId}"]` - - if (punchCard) { - selector = await getPunchCardActivity(page, activity) - - } else if (activity.name.toLowerCase().includes('membercenter')) { - - // Promotion - if (activity.priority === 1) { - selector = '#promo-item' - } else { - selector = `[data-bi-id="${activity.name}"]` - } - } - - // Wait for element to load - await page.waitForSelector(selector, { timeout: 5000 }) - // Click element, it will be opened in a new tab - await page.click(selector) - - // Select the new activity page - const activityPage = await getLatestTab(page) - - switch (activity.promotionType) { - // Quiz (Poll, Quiz or ABC) - case 'quiz': - switch (activity.pointProgressMax) { - // Poll or ABC (Usually 10 points) - case 10: - // Normal poll - if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) { - log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`) - await doPoll(activityPage) - } else { // ABC - log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`) - await doABC(activityPage) - } - break - - // This Or That Quiz (Usually 50 points) - case 50: - log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`) - await doThisOrThat(activityPage) - break - - // Quizzes are usually 30-40 points - default: - log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`) - await doQuiz(activityPage) - break + // Promotion + if (activity.priority === 1) { + selector = '#promo-item' + } else { + selector = `[data-bi-id="${activity.name}"]` } - break + } - // UrlReward (Visit) - case 'urlreward': - log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`) - await doUrlReward(activityPage) - break + // Wait for element to load + await page.waitForSelector(selector, { timeout: 5000 }) - // Misc, Usually UrlReward Type - default: - log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`) - await doUrlReward(activityPage) - break + // Click element, it will be opened in a new tab + await page.click(selector) + + // Cooldown + await this.bot.utils.wait(4000) + + // Select the new activity page + const activityPage = await this.bot.browser.utils.getLatestTab(page) + + // Wait for body to load + await activityPage.waitForSelector('body', { timeout: 10_000 }) + + switch (activity.promotionType) { + // Quiz (Poll, Quiz or ABC) + case 'quiz': + switch (activity.pointProgressMax) { + // Poll or ABC (Usually 10 points) + case 10: + // Normal poll + if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) { + this.bot.log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`) + await this.bot.activities.doPoll(activityPage) + } else { // ABC + this.bot.log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`) + await this.bot.activities.doABC(activityPage) + } + break + + // This Or That Quiz (Usually 50 points) + case 50: + this.bot.log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`) + await this.bot.activities.doThisOrThat(activityPage) + break + + // Quizzes are usually 30-40 points + default: + this.bot.log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`) + await this.bot.activities.doQuiz(activityPage) + break + } + break + + // UrlReward (Visit) + case 'urlreward': + this.bot.log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`) + await this.bot.activities.doUrlReward(activityPage) + break + + // Misc, Usually UrlReward Type + default: + this.bot.log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`) + await this.bot.activities.doUrlReward(activityPage) + break + } + + // Cooldown + await this.bot.utils.wait(2000) + } catch (error) { + this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error') } - - // Cooldown - await wait(1500) - } catch (error) { - log('ACTIVITY', 'An error occurred:' + error, 'error') } } + } \ No newline at end of file diff --git a/src/functions/activities/ABC.ts b/src/functions/activities/ABC.ts index 52cde7a..d64ed05 100644 --- a/src/functions/activities/ABC.ts +++ b/src/functions/activities/ABC.ts @@ -1,52 +1,52 @@ import { Page } from 'puppeteer' -import { refreshCheerio } from '../../browser/BrowserFunc' -import { getLatestTab } from '../../browser/BrowserUtil' -import { log } from '../../util/Logger' -import { randomNumber, wait } from '../../util/Utils' +import { Workers } from '../Workers' -export async function doABC(page: Page) { - log('ABC', 'Trying to complete poll') - try { - await wait(2000) - let $ = await refreshCheerio(page) +export class ABC extends Workers { - // Don't loop more than 15 in case unable to solve, would lock otherwise - const maxIterations = 15 - let i - for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { - await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 5000 }) + async doABC(page: Page) { + this.bot.log('ABC', 'Trying to complete poll') - const answers = $('.wk_OptionClickClass') - const answer = answers[randomNumber(0, 2)]?.attribs['id'] + try { + let $ = await this.bot.browser.func.refreshCheerio(page) - await page.waitForSelector(`#${answer}`, { visible: true, timeout: 5000 }) + // Don't loop more than 15 in case unable to solve, would lock otherwise + const maxIterations = 15 + let i + for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { + await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 5000 }) - await wait(2000) - await page.click(`#${answer}`) // Click answer + const answers = $('.wk_OptionClickClass') + const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id'] - await wait(4000) - await page.waitForSelector('div.wk_button', { visible: true, timeout: 5000 }) - await page.click('div.wk_button') // Click next question button + await page.waitForSelector(`#${answer}`, { visible: true, timeout: 5000 }) - page = await getLatestTab(page) - $ = await refreshCheerio(page) - await wait(1000) + await this.bot.utils.wait(2000) + await page.click(`#${answer}`) // Click answer + + await this.bot.utils.wait(4000) + await page.waitForSelector('div.wk_button', { visible: true, timeout: 5000 }) + await page.click('div.wk_button') // Click next question button + + page = await this.bot.browser.utils.getLatestTab(page) + $ = await this.bot.browser.func.refreshCheerio(page) + await this.bot.utils.wait(1000) + } + + await this.bot.utils.wait(4000) + await page.close() + + if (i === maxIterations) { + this.bot.log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn') + } else { + this.bot.log('ABC', 'Completed the ABC successfully') + } + + } catch (error) { + await page.close() + this.bot.log('ABC', 'An error occurred:' + error, 'error') } - - await wait(4000) - await page.close() - - if (i === maxIterations) { - log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn') - } else { - log('ABC', 'Completed the ABC successfully') - } - - } catch (error) { - await page.close() - log('ABC', 'An error occurred:' + error, 'error') } -} +} \ No newline at end of file diff --git a/src/functions/activities/Poll.ts b/src/functions/activities/Poll.ts index ad16f83..ad1d850 100644 --- a/src/functions/activities/Poll.ts +++ b/src/functions/activities/Poll.ts @@ -1,26 +1,29 @@ import { Page } from 'puppeteer' -import { log } from '../../util/Logger' -import { randomNumber, wait } from '../../util/Utils' +import { Workers } from '../Workers' -export async function doPoll(page: Page) { - log('POLL', 'Trying to complete poll') - try { - const buttonId = `#btoption${Math.floor(randomNumber(0, 1))}` +export class Poll extends Workers { - await page.waitForNetworkIdle({ timeout: 5000 }) - await page.waitForSelector(buttonId, { visible: true, timeout: 5000 }) - await wait(2000) + async doPoll(page: Page) { + this.bot.log('POLL', 'Trying to complete poll') - await page.click(buttonId) + try { + const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}` - await wait(4000) - await page.close() + await page.waitForSelector(buttonId, { visible: true, timeout: 5000 }) + await this.bot.utils.wait(2000) - log('POLL', 'Completed the poll successfully') - } catch (error) { - await page.close() - log('POLL', 'An error occurred:' + error, 'error') + await page.click(buttonId) + + await this.bot.utils.wait(4000) + await page.close() + + this.bot.log('POLL', 'Completed the poll successfully') + } catch (error) { + await page.close() + this.bot.log('POLL', 'An error occurred:' + error, 'error') + } } + } \ No newline at end of file diff --git a/src/functions/activities/Quiz.ts b/src/functions/activities/Quiz.ts index 0886cb3..3c53e9c 100644 --- a/src/functions/activities/Quiz.ts +++ b/src/functions/activities/Quiz.ts @@ -1,94 +1,91 @@ import { Page } from 'puppeteer' -import { getQuizData, waitForQuizRefresh } from '../../browser/BrowserFunc' -import { wait } from '../../util/Utils' -import { log } from '../../util/Logger' +import { Workers } from '../Workers' -export async function doQuiz(page: Page) { - log('QUIZ', 'Trying to complete quiz') - try { - await page.waitForNetworkIdle({ timeout: 5000 }) - await wait(2000) +export class Quiz extends Workers { - // Check if the quiz has been started or not - const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) - if (quizNotStarted) { - await page.click('#rqStartQuiz') - } else { - log('QUIZ', 'Quiz has already been started, trying to finish it') - } + async doQuiz(page: Page) { + this.bot.log('QUIZ', 'Trying to complete quiz') - await wait(2000) + try { + // Check if the quiz has been started or not + const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) + if (quizNotStarted) { + await page.click('#rqStartQuiz') + } else { + this.bot.log('QUIZ', 'Quiz has already been started, trying to finish it') + } - let quizData = await getQuizData(page) - const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining + await this.bot.utils.wait(2000) - // All questions - for (let question = 0; question < questionsRemaining; question++) { + let quizData = await this.bot.browser.func.getQuizData(page) + const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining - if (quizData.numberOfOptions === 8) { - const answers: string[] = [] + // All questions + for (let question = 0; question < questionsRemaining; question++) { - for (let i = 0; i < quizData.numberOfOptions; i++) { - const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) - const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) + if (quizData.numberOfOptions === 8) { + const answers: string[] = [] - if (answerAttribute && answerAttribute.toLowerCase() === 'true') { - answers.push(`#rqAnswerOption${i}`) + for (let i = 0; i < quizData.numberOfOptions; i++) { + const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) + const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) + + if (answerAttribute && answerAttribute.toLowerCase() === 'true') { + answers.push(`#rqAnswerOption${i}`) + } } - } - // Click the answers - for (const answer of answers) { - await page.waitForSelector(answer, { visible: true, timeout: 2000 }) + // Click the answers + for (const answer of answers) { + await page.waitForSelector(answer, { visible: true, timeout: 2000 }) - // Click the answer on page - await page.click(answer) - - const refreshSuccess = await waitForQuizRefresh(page) - if (!refreshSuccess) { - await page.close() - log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') - return - } - } - - // Other type quiz - } else if ([2, 3, 4].includes(quizData.numberOfOptions)) { - quizData = await getQuizData(page) // Refresh Quiz Data - const correctOption = quizData.correctAnswer - - for (let i = 0; i < quizData.numberOfOptions; i++) { - - const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) - const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) - - if (dataOption === correctOption) { // Click the answer on page - await page.click(`#rqAnswerOption${i}`) + await page.click(answer) - const refreshSuccess = await waitForQuizRefresh(page) + const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page) if (!refreshSuccess) { await page.close() - log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') + this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') return } } - } - await wait(2000) + // Other type quiz + } else if ([2, 3, 4].includes(quizData.numberOfOptions)) { + quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data + const correctOption = quizData.correctAnswer + + for (let i = 0; i < quizData.numberOfOptions; i++) { + + const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) + const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) + + if (dataOption === correctOption) { + // Click the answer on page + await page.click(`#rqAnswerOption${i}`) + + const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page) + if (!refreshSuccess) { + await page.close() + this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') + return + } + } + } + await this.bot.utils.wait(2000) + } } + // Done with + await this.bot.utils.wait(2000) + await page.close() + this.bot.log('QUIZ', 'Completed the quiz successfully') + } catch (error) { + await page.close() + this.bot.log('QUIZ', 'An error occurred:' + error, 'error') } - - // Done with - await wait(2000) - await page.close() - log('QUIZ', 'Completed the quiz successfully') - } catch (error) { - await page.close() - log('QUIZ', 'An error occurred:' + error, 'error') } } \ No newline at end of file diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts index 84e5b02..c50abb2 100644 --- a/src/functions/activities/Search.ts +++ b/src/functions/activities/Search.ts @@ -1,203 +1,230 @@ import { Page } from 'puppeteer' import axios from 'axios' -import { getLatestTab } from '../../browser/BrowserUtil' -import { getSearchPoints } from '../../browser/BrowserFunc' -import { log } from '../../util/Logger' -import { randomNumber, shuffleArray, wait } from '../../util/Utils' - -import { searchSettings } from '../../config.json' +import { Workers } from '../Workers' import { DashboardData, DashboardImpression } from '../../interface/DashboardData' import { GoogleTrends } from '../../interface/GoogleDailyTrends' import { GoogleSearch } from '../../interface/Search' -export async function doSearch(page: Page, data: DashboardData, mobile: boolean) { - log('SEARCH-BING', 'Starting bing searches') - const mobileData = data.userStatus.counters?.mobileSearch ? data.userStatus.counters.mobileSearch[0] : null // Mobile searches - const edgeData = data.userStatus.counters.pcSearch[1] as DashboardImpression // Edge searches - const genericData = data.userStatus.counters.pcSearch[0] as DashboardImpression // Normal searches +export class Search extends Workers { - let missingPoints = (mobile && mobileData) ? - (mobileData.pointProgressMax - mobileData.pointProgress) : - (edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress) + public async doSearch(page: Page, data: DashboardData, mobile: boolean) { + this.bot.log('SEARCH-BING', 'Starting bing searches') - if (missingPoints == 0) { - log('SEARCH-BING', `Bing searches for ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`) - return - } + const mobileData = data.userStatus.counters?.mobileSearch ? data.userStatus.counters.mobileSearch[0] : null // Mobile searches + const edgeData = data.userStatus.counters.pcSearch[1] as DashboardImpression // Edge searches + const genericData = data.userStatus.counters.pcSearch[0] as DashboardImpression // Normal searches - // Generate search queries - let googleSearchQueries = await getGoogleTrends(data.userProfile.attributes.country, missingPoints) - googleSearchQueries = shuffleArray(googleSearchQueries) - - // Deduplicate the search terms - googleSearchQueries = [...new Set(googleSearchQueries)] - - // Open a new tab - const browser = page.browser() - const searchPage = await browser.newPage() - - // Go to bing - await searchPage.goto('https://bing.com') - - let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search - - const queries: string[] = [] - googleSearchQueries.forEach(x => queries.push(x.topic, ...x.related)) - - // Loop over Google search queries - for (let i = 0; i < queries.length; i++) { - const query = queries[i] as string - - log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`) - - const newData = await bingSearch(page, searchPage, query) - - const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches - const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches - const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches - - const newMissingPoints = (mobile && newMobileData) ? - (newMobileData.pointProgressMax - newMobileData.pointProgress) : - (newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress) - - // If the new point amount is the same as before - if (newMissingPoints == missingPoints) { - maxLoop++ // Add to max loop - } else { // There has been a change in points - maxLoop = 0 // Reset the loop - } - - missingPoints = newMissingPoints + let missingPoints = (mobile && mobileData) ? + (mobileData.pointProgressMax - mobileData.pointProgress) : + (edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress) if (missingPoints == 0) { - break + this.bot.log('SEARCH-BING', `Bing searches for ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`) + return } - // If we didn't gain points for 10 iterations, assume it's stuck - if (maxLoop > 10) { - log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') - maxLoop = 0 // Reset to 0 so we can retry with related searches below - break + // Generate search queries + let googleSearchQueries = await this.getGoogleTrends(data.userProfile.attributes.country, missingPoints) + googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries) + + // Deduplicate the search terms + googleSearchQueries = [...new Set(googleSearchQueries)] + + // Open a new tab + const browser = page.browser() + const searchPage = await browser.newPage() + + // Go to bing + await searchPage.goto('https://bing.com') + + let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search + + const queries: string[] = [] + googleSearchQueries.forEach(x => queries.push(x.topic, ...x.related)) + + // Loop over Google search queries + for (let i = 0; i < queries.length; i++) { + const query = queries[i] as string + + this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`) + + const newData = await this.bingSearch(page, searchPage, query) + + const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches + const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches + const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches + + const newMissingPoints = (mobile && newMobileData) ? + (newMobileData.pointProgressMax - newMobileData.pointProgress) : + (newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress) + + // If the new point amount is the same as before + if (newMissingPoints == missingPoints) { + maxLoop++ // Add to max loop + } else { // There has been a change in points + maxLoop = 0 // Reset the loop + } + + missingPoints = newMissingPoints + + if (missingPoints == 0) { + break + } + + // If we didn't gain points for 10 iterations, assume it's stuck + if (maxLoop > 10) { + this.bot.log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') + maxLoop = 0 // Reset to 0 so we can retry with related searches below + break + } } - } - // If we still got remaining search queries, generate extra ones - if (missingPoints > 0) { - log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`) + // If we still got remaining search queries, generate extra ones + if (missingPoints > 0) { + this.bot.log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`) - let i = 0 - while (missingPoints > 0) { - const query = googleSearchQueries[i++] as GoogleSearch + let i = 0 + while (missingPoints > 0) { + const query = googleSearchQueries[i++] as GoogleSearch - // Get related search terms to the Google search queries - const relatedTerms = await getRelatedTerms(query?.topic) - if (relatedTerms.length > 3) { - // Search for the first 2 related terms - for (const term of relatedTerms.slice(1, 3)) { - log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${mobile}`) - const newData = await bingSearch(page, searchPage, query.topic) + // Get related search terms to the Google search queries + const relatedTerms = await this.getRelatedTerms(query?.topic) + if (relatedTerms.length > 3) { + // Search for the first 2 related terms + for (const term of relatedTerms.slice(1, 3)) { + this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${mobile}`) + const newData = await this.bingSearch(page, searchPage, query.topic) - const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches - const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches - const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches + const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches + const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches + const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches - const newMissingPoints = (mobile && newMobileData) ? - (newMobileData.pointProgressMax - newMobileData.pointProgress) : - (newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress) + const newMissingPoints = (mobile && newMobileData) ? + (newMobileData.pointProgressMax - newMobileData.pointProgress) : + (newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress) - // If the new point amount is the same as before - if (newMissingPoints == missingPoints) { - maxLoop++ // Add to max loop - } else { // There has been a change in points - maxLoop = 0 // Reset the loop - } + // If the new point amount is the same as before + if (newMissingPoints == missingPoints) { + maxLoop++ // Add to max loop + } else { // There has been a change in points + maxLoop = 0 // Reset the loop + } - missingPoints = newMissingPoints + missingPoints = newMissingPoints - // If we satisfied the searches - if (missingPoints == 0) { - break - } + // If we satisfied the searches + if (missingPoints == 0) { + break + } - // Try 5 more times, then we tried a total of 15 times, fair to say it's stuck - if (maxLoop > 5) { - log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn') - return + // Try 5 more times, then we tried a total of 15 times, fair to say it's stuck + if (maxLoop > 5) { + this.bot.log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn') + return + } } } } } + + this.bot.log('SEARCH-BING', 'Completed searches') } - log('SEARCH-BING', 'Completed searches') -} + private async bingSearch(page: Page, searchPage: Page, query: string) { + // Try a max of 5 times + for (let i = 0; i < 5; i++) { + try { + const searchBar = '#sb_form_q' + await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 }) + await searchPage.click(searchBar) // Focus on the textarea + await this.bot.utils.wait(500) + await searchPage.keyboard.down('Control') + await searchPage.keyboard.press('A') + await searchPage.keyboard.press('Backspace') + await searchPage.keyboard.up('Control') + await searchPage.keyboard.type(query) + await searchPage.keyboard.press('Enter') -async function bingSearch(page: Page, searchPage: Page, query: string) { - // Try a max of 5 times - for (let i = 0; i < 5; i++) { - try { - const searchBar = '#sb_form_q' - await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 }) - await searchPage.click(searchBar) // Focus on the textarea - await wait(500) - await searchPage.keyboard.down('Control') - await searchPage.keyboard.press('A') - await searchPage.keyboard.press('Backspace') - await searchPage.keyboard.up('Control') - await searchPage.keyboard.type(query) - await searchPage.keyboard.press('Enter') + if (this.bot.config.searchSettings.scrollRandomResults) { + await this.bot.utils.wait(2000) + await this.randomScroll(searchPage) + } - if (searchSettings.scrollRandomResults) { - await wait(2000) - await randomScroll(searchPage) + if (this.bot.config.searchSettings.clickRandomResults) { + await this.bot.utils.wait(2000) + await this.clickRandomLink(searchPage) + } + + await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max))) + + return await this.bot.browser.func.getSearchPoints(page) + + } catch (error) { + if (i === 5) { + this.bot.log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error') + break + + } + this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error') + this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn') + + await this.bot.utils.wait(4000) } - - if (searchSettings.clickRandomResults) { - await wait(2000) - await clickRandomLink(searchPage) - } - - await wait(Math.floor(randomNumber(10_000, 20_000))) - - return await getSearchPoints(page) - - } catch (error) { - if (i === 5) { - log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error') - break - - } - log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error') - log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn') - - await wait(4000) } + + this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error') + return await this.bot.browser.func.getSearchPoints(page) } - log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error') - return await getSearchPoints(page) -} + private async getGoogleTrends(geoLocale: string, queryCount: number): Promise { + const queryTerms: GoogleSearch[] = [] + let i = 0 -async function getGoogleTrends(geoLocale: string, queryCount: number): Promise { - const queryTerms: GoogleSearch[] = [] - let i = 0 + geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US' - geoLocale = (searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US' + this.bot.log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`) - log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`) + while (queryCount > queryTerms.length) { + i += 1 + const date = new Date() + date.setDate(date.getDate() - i) + const formattedDate = this.formatDate(date) - while (queryCount > queryTerms.length) { - i += 1 - const date = new Date() - date.setDate(date.getDate() - i) - const formattedDate = formatDate(date) + try { + const request = { + url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`, + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + const response = await axios(request) + + const data: GoogleTrends = JSON.parse((await response.data).slice(5)) + + for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) { + queryTerms.push({ + topic: topic.title.query.toLowerCase(), + related: topic.relatedQueries.map(x => x.query.toLocaleLowerCase()) + }) + } + + } catch (error) { + this.bot.log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error') + } + } + + return queryTerms + } + + private async getRelatedTerms(term: string): Promise { try { const request = { - url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`, + url: `https://api.bing.com/osjson.aspx?query=${term}`, method: 'GET', headers: { 'Content-Type': 'application/json' @@ -206,126 +233,99 @@ async function getGoogleTrends(geoLocale: string, queryCount: number): Promise x.query.toLocaleLowerCase()) - }) - } - + return response.data[1] as string[] } catch (error) { - log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error') + this.bot.log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error') } + return [] } - return queryTerms -} + private formatDate(date: Date): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') -async function getRelatedTerms(term: string): Promise { - try { - const request = { - url: `https://api.bing.com/osjson.aspx?query=${term}`, - method: 'GET', - headers: { - 'Content-Type': 'application/json' + return `${year}${month}${day}` + } + + private async randomScroll(page: Page) { + try { + // Press the arrow down key to scroll + for (let i = 0; i < this.bot.utils.randomNumber(5, 100); i++) { + await page.keyboard.press('ArrowDown') } + } catch (error) { + this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error') } - - const response = await axios(request) - - return response.data[1] as string[] - } catch (error) { - log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error') } - return [] -} -function formatDate(date: Date): string { - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') + private async clickRandomLink(page: Page) { + try { + const searchListingURL = new URL(page.url()) // Get searchPage info before clicking - return `${year}${month}${day}` -} + await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not -async function randomScroll(page: Page) { - try { - // Press the arrow down key to scroll - for (let i = 0; i < randomNumber(5, 100); i++) { - await page.keyboard.press('ArrowDown') - } - } catch (error) { - log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error') - } -} + // Wait for website to load + await this.bot.utils.wait(3000) -async function clickRandomLink(page: Page) { - try { - const searchListingURL = new URL(page.url()) // Get searchPage info before clicking + // Will get current tab if no new one is created + let lastTab = await this.bot.browser.utils.getLatestTab(page) - await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not + // Wait for the body of the new page to be loaded + await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { }) - // Wait for website to load - await wait(3000) - - // Will get current tab if no new one is created - let lastTab = await getLatestTab(page) - - // Wait for the body of the new page to be loaded - await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { }) - - // Check if the tab is closed or not - if (!lastTab.isClosed()) { - let lastTabURL = new URL(lastTab.url()) // Get new tab info - - // Check if the URL is different from the original one, don't loop more than 5 times. - let i = 0 - while (lastTabURL.href !== searchListingURL.href && i < 5) { - // If hostname is still bing, (Bing images/news etc) - if (lastTabURL.hostname == searchListingURL.hostname) { - await lastTab.goBack() - - lastTab = await getLatestTab(page) // Get last opened tab - - // If "goBack" didn't return to search listing (due to redirects) - if (lastTabURL.hostname !== searchListingURL.hostname) { - await lastTab.goto(searchListingURL.href) - } - - } else { // No longer on bing, likely opened a new tab, close this tab - lastTab = await getLatestTab(page) // Get last opened tab - lastTabURL = new URL(lastTab.url()) - - const tabs = await (page.browser()).pages() // Get all tabs open - - // If the browser has more than 3 tabs open, it has opened a new one, we need to close this one. - if (tabs.length > 3) { - // Make sure the page is still open! - if (!lastTab.isClosed()) { - await lastTab.close() - } - - } else if (lastTabURL.href !== searchListingURL.href) { + // Check if the tab is closed or not + if (!lastTab.isClosed()) { + let lastTabURL = new URL(lastTab.url()) // Get new tab info + // Check if the URL is different from the original one, don't loop more than 5 times. + let i = 0 + while (lastTabURL.href !== searchListingURL.href && i < 5) { + // If hostname is still bing, (Bing images/news etc) + if (lastTabURL.hostname == searchListingURL.hostname) { await lastTab.goBack() - lastTab = await getLatestTab(page) // Get last opened tab - lastTabURL = new URL(lastTab.url()) + lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab // If "goBack" didn't return to search listing (due to redirects) if (lastTabURL.hostname !== searchListingURL.hostname) { await lastTab.goto(searchListingURL.href) } + + } else { // No longer on bing, likely opened a new tab, close this tab + lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab + lastTabURL = new URL(lastTab.url()) + + const tabs = await (page.browser()).pages() // Get all tabs open + + // If the browser has more than 3 tabs open, it has opened a new one, we need to close this one. + if (tabs.length > 3) { + // Make sure the page is still open! + if (!lastTab.isClosed()) { + await lastTab.close() + } + + } else if (lastTabURL.href !== searchListingURL.href) { + + await lastTab.goBack() + + lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab + lastTabURL = new URL(lastTab.url()) + + // If "goBack" didn't return to search listing (due to redirects) + if (lastTabURL.hostname !== searchListingURL.hostname) { + await lastTab.goto(searchListingURL.href) + } + } } } - } - lastTab = await getLatestTab(page) // Finally update the lastTab var again - i++ + lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again + i++ + } + } catch (error) { + this.bot.log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error') } - } catch (error) { - log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error') } + } \ No newline at end of file diff --git a/src/functions/activities/ThisOrThat.ts b/src/functions/activities/ThisOrThat.ts index a7880ea..cd8768d 100644 --- a/src/functions/activities/ThisOrThat.ts +++ b/src/functions/activities/ThisOrThat.ts @@ -1,34 +1,33 @@ import { Page } from 'puppeteer' -import { wait } from '../../util/Utils' -import { log } from '../../util/Logger' -import { getQuizData } from '../../browser/BrowserFunc' +import { Workers } from '../Workers' -export async function doThisOrThat(page: Page) { - log('THIS-OR-THAT', 'Trying to complete ThisOrThat') - try { - await page.waitForNetworkIdle({ timeout: 5000 }) - await wait(2000) +export class ThisOrThat extends Workers { - // Check if the quiz has been started or not - const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) - if (quizNotStarted) { - await page.click('#rqStartQuiz') - } else { - log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it') + async doThisOrThat(page: Page) { + this.bot.log('THIS-OR-THAT', 'Trying to complete ThisOrThat') + + try { + // Check if the quiz has been started or not + const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) + if (quizNotStarted) { + await page.click('#rqStartQuiz') + } else { + this.bot.log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it') + } + + await this.bot.utils.wait(2000) + + // Solving + const quizData = await this.bot.browser.func.getQuizData(page) + quizData // correctAnswer property is always null? + + this.bot.log('THIS-OR-THAT', 'Completed the ThisOrthat successfully') + } catch (error) { + await page.close() + this.bot.log('THIS-OR-THAT', 'An error occurred:' + error, 'error') } - - await wait(2000) - - // Solving - const quizData = await getQuizData(page) - quizData // correctAnswer property is always null? - - log('THIS-OR-THAT', 'Completed the ThisOrthat successfully') - } catch (error) { - await page.close() - log('THIS-OR-THAT', 'An error occurred:' + error, 'error') } } \ No newline at end of file diff --git a/src/functions/activities/UrlReward.ts b/src/functions/activities/UrlReward.ts index 0d29bfb..b65209f 100644 --- a/src/functions/activities/UrlReward.ts +++ b/src/functions/activities/UrlReward.ts @@ -1,21 +1,22 @@ import { Page } from 'puppeteer' -import { wait } from '../../util/Utils' -import { log } from '../../util/Logger' +import { Workers } from '../Workers' -export async function doUrlReward(page: Page) { - log('URL-REWARD', 'Trying to complete UrlReward') - try { - // After waiting, close the page - await page.waitForNetworkIdle({ timeout: 10_000 }) - await wait(2000) - await page.close() +export class UrlReward extends Workers { - log('URL-REWARD', 'Completed the UrlReward successfully') - } catch (error) { - await page.close() - log('URL-REWARD', 'An error occurred:' + error, 'error') + async doUrlReward(page: Page) { + this.bot.log('URL-REWARD', 'Trying to complete UrlReward') + + try { + // After waiting, close the page + await page.close() + + this.bot.log('URL-REWARD', 'Completed the UrlReward successfully') + } catch (error) { + await page.close() + this.bot.log('URL-REWARD', 'An error occurred:' + error, 'error') + } } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ff72b59..099bd88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,40 +1,60 @@ import cluster from 'cluster' import Browser from './browser/Browser' -import { getDashboardData, getEarnablePoints, goHome } from './browser/BrowserFunc' +import BrowserFunc from './browser/BrowserFunc' +import BrowserUtil from './browser/BrowserUtil' import { log } from './util/Logger' -import { loadAccounts } from './util/Account' -import { chunkArray } from './util/Utils' +import Util from './util/Utils' +import { loadAccounts, loadConfig } from './util/Load' -import { login } from './functions/Login' -import { doDailySet, doMorePromotions, doPunchCard } from './functions/Workers' -import { doSearch } from './functions/activities/Search' +import { Login } from './functions/Login' +import { Workers } from './functions/Workers' +import Activities from './functions/Activities' import { Account } from './interface/Account' -import { runOnZeroPoints, workers, clusters } from './config.json' - // Main bot class -class MicrosoftRewardsBot { - private activeWorkers: number = clusters +export class MicrosoftRewardsBot { + public log: typeof log + public config + public utils: Util + public activities: Activities = new Activities(this) + public browser: { + func: BrowserFunc, + utils: BrowserUtil + } + private collectedPoints: number = 0 - private browserFactory: Browser = new Browser() + private activeWorkers: number + private browserFactory: Browser = new Browser(this) private accounts: Account[] + private workers: Workers + private login = new Login(this) constructor() { + this.log = log + this.accounts = [] + this.utils = new Util() + this.workers = new Workers(this) + this.browser = { + func: new BrowserFunc(this), + utils: new BrowserUtil(this) + } + this.config = loadConfig() + this.activeWorkers = this.config.clusters } async initialize() { - this.accounts = await loadAccounts() + this.accounts = loadAccounts() } async run() { - log('MAIN', `Bot started with ${clusters} clusters`) + log('MAIN', `Bot started with ${this.config.clusters} clusters`) // Only cluster when there's more than 1 cluster demanded - if (clusters > 1) { + if (this.config.clusters > 1) { if (cluster.isPrimary) { this.runMaster() } else { @@ -48,7 +68,7 @@ class MicrosoftRewardsBot { private runMaster() { log('MAIN-PRIMARY', 'Primary process started') - const accountChunks = chunkArray(this.accounts, clusters) + const accountChunks = this.utils.chunkArray(this.accounts, this.config.clusters) for (let i = 0; i < accountChunks.length; i++) { const worker = cluster.fork() @@ -71,7 +91,6 @@ class MicrosoftRewardsBot { private runWorker() { log('MAIN-WORKER', `Worker ${process.pid} spawned`) - // Receive the chunk of accounts from the master process.on('message', async ({ chunk }) => { await this.runTasks(chunk) @@ -86,7 +105,7 @@ class MicrosoftRewardsBot { await this.Desktop(account) // If runOnZeroPoints is false and 0 points to earn, stop and try the next account - if (!runOnZeroPoints && this.collectedPoints === 0) { + if (!this.config.runOnZeroPoints && this.collectedPoints === 0) { continue } @@ -119,22 +138,22 @@ class MicrosoftRewardsBot { log('MAIN', 'Starting DESKTOP browser') // Login into MS Rewards - await login(page, account.email, account.password) + await this.login.login(page, account.email, account.password) - const wentHome = await goHome(page) + const wentHome = await this.browser.func.goHome(page) if (!wentHome) { throw log('MAIN', 'Unable to get dashboard page', 'error') } - const data = await getDashboardData(page) + const data = await this.browser.func.getDashboardData(page) log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`) - const earnablePoints = await getEarnablePoints(data) + const earnablePoints = await this.browser.func.getEarnablePoints(data) this.collectedPoints = earnablePoints log('MAIN-POINTS', `You can earn ${earnablePoints} points today`) // If runOnZeroPoints is false and 0 points to earn, don't continue - if (!runOnZeroPoints && this.collectedPoints === 0) { + if (!this.config.runOnZeroPoints && this.collectedPoints === 0) { log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping') // Close desktop browser @@ -142,23 +161,23 @@ class MicrosoftRewardsBot { } // Complete daily set - if (workers.doDailySet) { - await doDailySet(page, data) + if (this.config.workers.doDailySet) { + await this.workers.doDailySet(page, data) } // Complete more promotions - if (workers.doMorePromotions) { - await doMorePromotions(page, data) + if (this.config.workers.doMorePromotions) { + await this.workers.doMorePromotions(page, data) } // Complete punch cards - if (workers.doPunchCards) { - await doPunchCard(page, data) + if (this.config.workers.doPunchCards) { + await this.workers.doPunchCard(page, data) } // Do desktop searches - if (workers.doDesktopSearch) { - await doSearch(page, data, false) + if (this.config.workers.doDesktopSearch) { + await this.activities.doSearch(page, data, false) } // Close desktop browser @@ -182,11 +201,11 @@ class MicrosoftRewardsBot { log('MAIN', 'Starting MOBILE browser') // Login into MS Rewards - await login(page, account.email, account.password) + await this.login.login(page, account.email, account.password) - await goHome(page) + await this.browser.func.goHome(page) - const data = await getDashboardData(page) + const data = await this.browser.func.getDashboardData(page) // If no mobile searches data found, stop (Does not exist on new accounts) if (!data.userStatus.counters.mobileSearch) { @@ -197,12 +216,12 @@ class MicrosoftRewardsBot { } // Do mobile searches - if (workers.doMobileSearch) { - await doSearch(page, data, true) + if (this.config.workers.doMobileSearch) { + await this.activities.doSearch(page, data, true) } // Fetch new points - const earnablePoints = await getEarnablePoints(data, page) + const earnablePoints = await this.browser.func.getEarnablePoints(data, page) // If the new earnable is 0, means we got all the points, else retract this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints) diff --git a/src/interface/Config.ts b/src/interface/Config.ts new file mode 100644 index 0000000..1709041 --- /dev/null +++ b/src/interface/Config.ts @@ -0,0 +1,35 @@ +export interface Config { + baseURL: string; + sessionPath: string; + headless: boolean; + runOnZeroPoints: boolean; + clusters: number; + workers: Workers; + searchSettings: SearchSettings; + webhook: Webhook; +} + +export interface SearchSettings { + useGeoLocaleQueries: boolean; + scrollRandomResults: boolean; + clickRandomResults: boolean; + searchDelay: SearchDelay; +} + +export interface SearchDelay { + min: number; + max: number; +} + +export interface Webhook { + enabled: boolean; + url: string; +} + +export interface Workers { + doDailySet: boolean; + doMorePromotions: boolean; + doPunchCards: boolean; + doDesktopSearch: boolean; + doMobileSearch: boolean; +} diff --git a/src/util/Account.ts b/src/util/Load.ts similarity index 55% rename from src/util/Account.ts rename to src/util/Load.ts index f904b7c..0742881 100644 --- a/src/util/Account.ts +++ b/src/util/Load.ts @@ -1,9 +1,11 @@ -import * as fs from 'fs' +import fs from 'fs' import path from 'path' import { Account } from '../interface/Account' +import { Config } from '../interface/Config' -export async function loadAccounts(): Promise { + +export function loadAccounts(): Account[] { try { let file = 'accounts.json' @@ -19,4 +21,15 @@ export async function loadAccounts(): Promise { } catch (error) { throw new Error(error as string) } +} + +export function loadConfig(): Config { + try { + const configDir = path.join(__dirname, '../', 'config.json') + const config = fs.readFileSync(configDir, 'utf-8') + + return JSON.parse(config) + } catch (error) { + throw new Error(error as string) + } } \ No newline at end of file diff --git a/src/util/Utils.ts b/src/util/Utils.ts index 4a67e44..14c7336 100644 --- a/src/util/Utils.ts +++ b/src/util/Utils.ts @@ -1,38 +1,42 @@ -export async function wait(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) -} +export default class Util { -export function getFormattedDate(ms = Date.now()): string { - const today = new Date(ms) - const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0 - const day = String(today.getDate()).padStart(2, '0') - const year = today.getFullYear() - - return `${month}/${day}/${year}` -} - -export function shuffleArray(array: T[]): T[] { - const shuffledArray = array.slice() - - shuffledArray.sort(() => Math.random() - 0.5) - - return shuffledArray -} - -export function randomNumber(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min -} - -export function chunkArray(arr: T[], numChunks: number): T[][] { - const chunkSize = Math.ceil(arr.length / numChunks) - const chunks: T[][] = [] - - for (let i = 0; i < arr.length; i += chunkSize) { - const chunk = arr.slice(i, i + chunkSize) - chunks.push(chunk) + async wait(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) + } + + getFormattedDate(ms = Date.now()): string { + const today = new Date(ms) + const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0 + const day = String(today.getDate()).padStart(2, '0') + const year = today.getFullYear() + + return `${month}/${day}/${year}` + } + + shuffleArray(array: T[]): T[] { + const shuffledArray = array.slice() + + shuffledArray.sort(() => Math.random() - 0.5) + + return shuffledArray + } + + randomNumber(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min + } + + chunkArray(arr: T[], numChunks: number): T[][] { + const chunkSize = Math.ceil(arr.length / numChunks) + const chunks: T[][] = [] + + for (let i = 0; i < arr.length; i += chunkSize) { + const chunk = arr.slice(i, i + chunkSize) + chunks.push(chunk) + } + + return chunks } - return chunks } \ No newline at end of file diff --git a/src/util/Webhook.ts b/src/util/Webhook.ts index a06fb84..83726b7 100644 --- a/src/util/Webhook.ts +++ b/src/util/Webhook.ts @@ -1,10 +1,13 @@ import axios from 'axios' -import { webhook } from '../config.json' +import { loadConfig } from './Load' + export async function Webhook(content: string) { + const webhook = loadConfig().webhook + if (!webhook.enabled || webhook.url.length < 10) return - + const request = { method: 'POST', url: webhook.url, diff --git a/tsconfig.json b/tsconfig.json index 2169095..34a0dde 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -64,7 +64,8 @@ }, "include": [ "src/**/*.ts", - "src/accounts.json" + "src/accounts.json", + "src/config.json" ], "exclude": [ "node_modules"