From 9ea7f5c452d1c670dd0709f5403b21b1f9759460 Mon Sep 17 00:00:00 2001 From: TheNetsky <56271887+TheNetsky@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:33:48 +0100 Subject: [PATCH] 1.4.0 - Switched from Puppeteer to Playwright - Fixed mobile searches not working - Added fingerprint saving in config - Added mobile search retry in config --- README.md | 2 +- package.json | 10 ++-- src/browser/Browser.ts | 71 ++++++++++++-------------- src/browser/BrowserFunc.ts | 14 ++--- src/browser/BrowserUtil.ts | 12 ++--- src/config.json | 6 ++- src/functions/Activities.ts | 2 +- src/functions/Login.ts | 8 +-- src/functions/Workers.ts | 10 ++-- src/functions/activities/ABC.ts | 8 +-- src/functions/activities/Poll.ts | 4 +- src/functions/activities/Quiz.ts | 10 ++-- src/functions/activities/Search.ts | 62 ++++++++++++---------- src/functions/activities/ThisOrThat.ts | 4 +- src/functions/activities/UrlReward.ts | 2 +- src/index.ts | 55 ++++++++++---------- src/interface/Config.ts | 2 + src/util/Load.ts | 57 ++++++++++++++++++++- src/util/UserAgent.ts | 2 +- 19 files changed, 199 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 67be7a5..aa2f4c5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Microsoft-Rewards-Script -Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Puppeteer. +Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Playwright. Under development, however mainly for personal use! diff --git a/package.json b/package.json index ecb8720..3b43c18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microsoft-rewards-script", - "version": "1.3.2", + "version": "1.4.0", "description": "Automatically do tasks for Microsoft Rewards but in TS!", "main": "index.js", "engines": { @@ -19,7 +19,7 @@ "Bot", "Script", "TypeScript", - "Puppeteer", + "Playwright", "Cheerio" ], "author": "Netsky", @@ -33,9 +33,9 @@ "dependencies": { "axios": "^1.6.2", "cheerio": "^1.0.0-rc.12", - "fingerprint-generator": "^2.1.45", - "fingerprint-injector": "^2.1.45", - "puppeteer": "^21.6.1", + "fingerprint-generator": "^2.1.46", + "fingerprint-injector": "^2.1.46", + "playwright": "^1.40.1", "ts-node": "^10.9.2" } } diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index 60a0710..6dbc042 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -1,9 +1,11 @@ -import puppeteer from 'puppeteer' -import { FingerprintInjector } from 'fingerprint-injector' +import playwright from 'playwright' +import { BrowserContext } from 'playwright' + +import { newInjectedContext } from 'fingerprint-injector' import { FingerprintGenerator } from 'fingerprint-generator' import { MicrosoftRewardsBot } from '../index' -import { loadSesion } from '../util/Load' +import { loadSessionData, saveFingerprintData } from '../util/Load' import { AccountProxy } from '../interface/Account' @@ -13,63 +15,54 @@ https://botcheck.luminati.io/ http://f.vision/ */ - class Browser { private bot: MicrosoftRewardsBot - private usedUserAgents: string[] = [] constructor(bot: MicrosoftRewardsBot) { this.bot = bot } - async createBrowser(email: string, proxy: AccountProxy) { - // const userAgent = await getUserAgent(isMobile) - - const browser = await puppeteer.launch({ - headless: this.bot.config.headless ? 'new' : false, - userDataDir: await loadSesion(this.bot.config.sessionPath, email), + async createBrowser(proxy: AccountProxy, email: string): Promise { + const browser = await playwright.chromium.launch({ + //channel: 'msedge', // Uses Edge instead of chrome + headless: this.bot.config.headless, + ...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }), args: [ '--no-sandbox', '--mute-audio', '--disable-setuid-sandbox', '--ignore-certificate-errors', '--ignore-certificate-errors-spki-list', - '--ignore-ssl-errors', - proxy.url ? `--proxy-server=${proxy.url}:${proxy.port}` : '' + '--ignore-ssl-errors' ] }) - let fingerPrintData = new FingerprintGenerator().getFingerprint({ + const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint) + + const fingerpint = sessionData.fingerprint ? sessionData.fingerprint : this.generateFingerprint() + + const context = await newInjectedContext(browser, { fingerprint: fingerpint }) + + await context.addCookies(sessionData.cookies) + + if (this.bot.config.saveFingerprint) { + await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerpint) + } + + this.bot.log('BROWSER', `Created browser with User-Agent: "${fingerpint.fingerprint.navigator.userAgent}"`) + + return context + } + + generateFingerprint() { + const fingerPrintData = new FingerprintGenerator().getFingerprint({ devices: this.bot.isMobile ? ['mobile'] : ['desktop'], operatingSystems: this.bot.isMobile ? ['android'] : ['windows'], browsers: ['edge'] }) - if (this.usedUserAgents) { - while (this.usedUserAgents.includes(fingerPrintData.fingerprint.navigator.userAgent)) { - fingerPrintData = new FingerprintGenerator().getFingerprint({ - devices: this.bot.isMobile ? ['mobile'] : ['desktop'], - operatingSystems: this.bot.isMobile ? ['android'] : ['windows'], - browsers: ['edge'] - }) - } - } - - this.usedUserAgents.push(fingerPrintData.fingerprint.navigator.userAgent) - - // Modify the newPage function to attach the fingerprint - const originalNewPage = browser.newPage - browser.newPage = async function () { - const page = await originalNewPage.apply(browser) - await new FingerprintInjector().attachFingerprintToPuppeteer(page, fingerPrintData) - return page - } - - this.bot.log('BROWSER', `Created browser with User-Agent: "${fingerPrintData.fingerprint.navigator.userAgent}"`) - - return browser + return fingerPrintData } - } -export default Browser \ No newline at end of file +export default Browser diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index d73099f..5d0c876 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { CheerioAPI, load } from 'cheerio' import { MicrosoftRewardsBot } from '../index' @@ -17,7 +17,7 @@ export default class BrowserFunc { /** * Navigate the provided page to rewards homepage - * @param {Page} page Puppeteer page + * @param {Page} page Playwright page */ async goHome(page: Page) { @@ -37,7 +37,7 @@ export default class BrowserFunc { await this.bot.browser.utils.tryDismissCookieBanner(page) // Check if account is suspended - const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { visible: true, timeout: 2000 }).then(() => true).catch(() => false) + 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!') @@ -89,7 +89,7 @@ export default class BrowserFunc { } // Reload the page to get new data - await this.bot.homePage.reload({ waitUntil: 'networkidle2' }) + await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' }) const scriptContent = await this.bot.homePage.evaluate(() => { const scripts = Array.from(document.querySelectorAll('script')) @@ -180,7 +180,7 @@ export default class BrowserFunc { /** * Parse quiz data from provided page - * @param {Page} page Puppeteer page + * @param {Page} page Playwright page * @returns {QuizData} Quiz data object */ async getQuizData(page: Page): Promise { @@ -214,7 +214,7 @@ export default class BrowserFunc { async waitForQuizRefresh(page: Page): Promise { try { - await page.waitForSelector('span.rqMCredits', { visible: true, timeout: 10_000 }) + await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 }) await this.bot.utils.wait(2000) return true @@ -226,7 +226,7 @@ export default class BrowserFunc { async checkQuizCompleted(page: Page): Promise { try { - await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 2000 }) + await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 }) await this.bot.utils.wait(2000) return true diff --git a/src/browser/BrowserUtil.ts b/src/browser/BrowserUtil.ts index 22ba237..7cd8d97 100644 --- a/src/browser/BrowserUtil.ts +++ b/src/browser/BrowserUtil.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { MicrosoftRewardsBot } from '../index' @@ -24,7 +24,7 @@ export default class BrowserUtil { for (const button of buttons) { try { - const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 }) + const element = await page.waitForSelector(button.selector, { state: 'visible', timeout: 1000 }) if (element) { await element.click() result = true @@ -73,8 +73,8 @@ export default class BrowserUtil { try { await this.bot.utils.wait(500) - const browser = page.browser() - const pages = await browser.pages() + const browser = page.context() + const pages = browser.pages() const newTab = pages[pages.length - 1] if (newTab) { @@ -89,8 +89,8 @@ export default class BrowserUtil { async getTabs(page: Page) { try { - const browser = page.browser() - const pages = await browser.pages() + const browser = page.context() + const pages = browser.pages() const homeTab = pages[1] let homeTabURL: URL diff --git a/src/config.json b/src/config.json index b081fbe..fbb2fec 100644 --- a/src/config.json +++ b/src/config.json @@ -2,8 +2,9 @@ "baseURL": "https://rewards.bing.com", "sessionPath": "sessions", "headless": false, - "runOnZeroPoints": false, + "runOnZeroPoints": true, "clusters": 1, + "saveFingerprint": false, "workers": { "doDailySet": true, "doMorePromotions": true, @@ -18,7 +19,8 @@ "searchDelay": { "min": 10000, "max": 20000 - } + }, + "retryMobileSearch": true }, "webhook": { "enabled": false, diff --git a/src/functions/Activities.ts b/src/functions/Activities.ts index 8901f50..ffa30bb 100644 --- a/src/functions/Activities.ts +++ b/src/functions/Activities.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { MicrosoftRewardsBot } from '../index' diff --git a/src/functions/Login.ts b/src/functions/Login.ts index 88f4524..15fb47d 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import readline from 'readline' import { MicrosoftRewardsBot } from '../index' @@ -26,13 +26,13 @@ export class Login { if (!isLoggedIn) { // Check if account is locked - const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 10_000 }).then(() => true).catch(() => false) + const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 10_000 }).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 page.waitForSelector('#loginHeader', { state: 'visible', timeout: 10_000 }) await this.execLogin(page, email, password) this.bot.log('LOGIN', 'Logged into Microsoft successfully') @@ -59,7 +59,7 @@ export class Login { this.bot.log('LOGIN', 'Email entered successfully') try { - await page.waitForSelector('#i0118', { visible: true, timeout: 2000 }) + await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) await this.bot.utils.wait(2000) await page.type('#i0118', password) diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index 93df861..5b4f47d 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData' @@ -58,13 +58,13 @@ export class Workers { await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL }) // Wait for new page to load, max 10 seconds, however try regardless in case of error - await page.waitForNetworkIdle({ timeout: 5_000 }).catch(() => { }) + await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { }) await this.solveActivities(page, activitiesUncompleted, punchCard) page = await this.bot.browser.utils.getLatestTab(page) - const pages = await (page.browser()).pages() + const pages = page.context().pages() if (pages.length > 3) { await page.close() @@ -118,7 +118,7 @@ export class Workers { // Reselect the worker page activityPage = await this.bot.browser.utils.getLatestTab(activityPage) - const pages = await activityPage.browser().pages() + const pages = activityPage.context().pages() if (pages.length > 3) { await activityPage.close() @@ -152,7 +152,7 @@ export class Workers { Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster, if it didn't then it gave enough time for the page to load. */ - await activityPage.waitForNetworkIdle({ timeout: 10_000 }).catch(() => { }) + await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { }) await this.bot.utils.wait(5000) switch (activity.promotionType) { diff --git a/src/functions/activities/ABC.ts b/src/functions/activities/ABC.ts index 4aa02cf..2d27641 100644 --- a/src/functions/activities/ABC.ts +++ b/src/functions/activities/ABC.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { Workers } from '../Workers' @@ -15,18 +15,18 @@ export class ABC extends Workers { const maxIterations = 15 let i for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { - await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 10_000 }) + await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 }) const answers = $('.wk_OptionClickClass') const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id'] - await page.waitForSelector(`#${answer}`, { visible: true, timeout: 10_000 }) + await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 }) 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: 10_000 }) + await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 }) await page.click('div.wk_button') // Click next question button page = await this.bot.browser.utils.getLatestTab(page) diff --git a/src/functions/activities/Poll.ts b/src/functions/activities/Poll.ts index 0450458..d9dda7c 100644 --- a/src/functions/activities/Poll.ts +++ b/src/functions/activities/Poll.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { Workers } from '../Workers' @@ -11,7 +11,7 @@ export class Poll extends Workers { try { const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}` - await page.waitForSelector(buttonId, { visible: true, timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not + await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not await this.bot.utils.wait(2000) await page.click(buttonId) diff --git a/src/functions/activities/Quiz.ts b/src/functions/activities/Quiz.ts index 90957a8..a94b32a 100644 --- a/src/functions/activities/Quiz.ts +++ b/src/functions/activities/Quiz.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { Workers } from '../Workers' @@ -10,7 +10,7 @@ export class Quiz extends Workers { try { // Check if the quiz has been started or not - const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 2000 }).then(() => true).catch(() => false) + const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false) if (quizNotStarted) { await page.click('#rqStartQuiz') } else { @@ -29,7 +29,7 @@ export class Quiz extends Workers { const answers: string[] = [] for (let i = 0; i < quizData.numberOfOptions; i++) { - const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 }) + const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) if (answerAttribute && answerAttribute.toLowerCase() === 'true') { @@ -39,7 +39,7 @@ export class Quiz extends Workers { // Click the answers for (const answer of answers) { - await page.waitForSelector(answer, { visible: true, timeout: 2000 }) + await page.waitForSelector(answer, { state: 'visible', timeout: 2000 }) // Click the answer on page await page.click(answer) @@ -59,7 +59,7 @@ export class Quiz extends Workers { for (let i = 0; i < quizData.numberOfOptions; i++) { - const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 }) + const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) if (dataOption === correctOption) { diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts index c700855..5510a2c 100644 --- a/src/functions/activities/Search.ts +++ b/src/functions/activities/Search.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import axios from 'axios' import { Workers } from '../Workers' @@ -63,6 +63,12 @@ export class Search extends Workers { break } + // Only for mobile searches + if (maxLoop > 3 && this.bot.isMobile) { + this.bot.log('SEARCH-BING-MOBILE', 'Search didn\'t gain point for 3 iterations, likely bad User-Agent', 'warn') + 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') @@ -71,6 +77,11 @@ export class Search extends Workers { } } + // Only for mobile searches + if (missingPoints > 0 && this.bot.isMobile) { + return + } + // 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`) @@ -121,7 +132,7 @@ export class Search extends Workers { for (let i = 0; i < 5; i++) { try { const searchBar = '#sb_form_q' - await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 }) + await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 }) await searchPage.click(searchBar) // Focus on the textarea await this.bot.utils.wait(500) await searchPage.keyboard.down('Control') @@ -155,20 +166,8 @@ export class Search extends Workers { this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn') // Reset the tabs - const browser = searchPage.browser() - const tabs = await browser.pages() const lastTab = await this.bot.browser.utils.getLatestTab(searchPage) - - if (tabs.length === 4) { - await lastTab.close() - - } else if (tabs.length === 2) { - const newPage = await browser.newPage() - await newPage.goto(this.searchPageURL) - - } else { - await lastTab.goBack() - } + await this.closeTabs(lastTab, this.searchPageURL) await this.bot.utils.wait(4000) } @@ -276,19 +275,8 @@ export class Search extends Workers { // 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) { - const browser = page.browser() - const tabs = await browser.pages() - if (tabs.length === 4) { - await lastTab.close() - - } else if (tabs.length === 2) { - const newPage = await browser.newPage() - await newPage.goto(searchListingURL.href) - - } else { - await lastTab.goBack() - } + await this.closeTabs(lastTab, searchListingURL.href) // End of loop, refresh lastPage lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again @@ -301,6 +289,26 @@ export class Search extends Workers { } } + private async closeTabs(lastTab: Page, url: string) { + const browser = lastTab.context() + const tabs = browser.pages() + + // If more than 3 tabs are open, close the last tab + if (tabs.length > 2) { + await lastTab.close() + + // If only 1 tab is open, open a new one to search in + } else if (tabs.length === 1) { + const newPage = await browser.newPage() + await newPage.goto(url) + + // Else go back one page + } else { + await lastTab.goBack() + } + } + + private calculatePoints(counters: Counters) { const mobileData = counters.mobileSearch?.[0] // Mobile searches const genericData = counters.pcSearch?.[0] // Normal searches diff --git a/src/functions/activities/ThisOrThat.ts b/src/functions/activities/ThisOrThat.ts index 7613121..0c4f926 100644 --- a/src/functions/activities/ThisOrThat.ts +++ b/src/functions/activities/ThisOrThat.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { Workers } from '../Workers' @@ -11,7 +11,7 @@ export class ThisOrThat extends Workers { try { // Check if the quiz has been started or not - const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 2000 }).then(() => true).catch(() => false) + const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false) if (quizNotStarted) { await page.click('#rqStartQuiz') } else { diff --git a/src/functions/activities/UrlReward.ts b/src/functions/activities/UrlReward.ts index 86bc637..1347de1 100644 --- a/src/functions/activities/UrlReward.ts +++ b/src/functions/activities/UrlReward.ts @@ -1,4 +1,4 @@ -import { Page } from 'puppeteer' +import { Page } from 'playwright' import { Workers } from '../Workers' diff --git a/src/index.ts b/src/index.ts index cc6ae54..2133d75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import cluster from 'cluster' -import { Page } from 'puppeteer' +import { BrowserContext, Page } from 'playwright' import Browser from './browser/Browser' import BrowserFunc from './browser/BrowserFunc' @@ -7,7 +7,7 @@ import BrowserUtil from './browser/BrowserUtil' import { log } from './util/Logger' import Util from './util/Utils' -import { loadAccounts, loadConfig } from './util/Load' +import { loadAccounts, loadConfig, saveSessionData } from './util/Load' import { Login } from './functions/Login' import { Workers } from './functions/Workers' @@ -125,18 +125,8 @@ export class MicrosoftRewardsBot { // Desktop async Desktop(account: Account) { - const browser = await this.browserFactory.createBrowser(account.email, account.proxy) + const browser = await this.browserFactory.createBrowser(account.proxy, account.email) this.homePage = await browser.newPage() - let pages = await browser.pages() - - // If for some reason the browser initializes with more than 2 pages, close these - while (pages.length > 2) { - await pages[0]?.close() - pages = await browser.pages() - } - - // Log into proxy - await this.homePage.authenticate({ username: account.proxy.username, password: account.proxy.password }) log('MAIN', 'Starting DESKTOP browser') @@ -156,7 +146,7 @@ export class MicrosoftRewardsBot { log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!') // Close desktop browser - return await browser.close() + return await this.closeBrowser(browser, account.email) } // Open a new tab to where the tasks are going to be completed @@ -185,25 +175,20 @@ export class MicrosoftRewardsBot { await this.activities.doSearch(workerPage, data) } + // Save cookies + const cookies = await browser.cookies() + await saveSessionData(this.config.sessionPath, account.email, this.isMobile, cookies) + // Close desktop browser - await browser.close() + return await this.closeBrowser(browser, account.email) } // Mobile async Mobile(account: Account) { this.isMobile = true - const browser = await this.browserFactory.createBrowser(account.email, account.proxy) + const browser = await this.browserFactory.createBrowser(account.proxy, account.email) this.homePage = await browser.newPage() - let pages = await browser.pages() - - // If for some reason the browser initializes with more than 2 pages, close these - while (pages.length > 2) { - await pages[0]?.close() - pages = await browser.pages() - } - // Log into proxy - await this.homePage.authenticate({ username: account.proxy.username, password: account.proxy.password }) log('MAIN', 'Starting MOBILE browser') @@ -218,7 +203,7 @@ export class MicrosoftRewardsBot { log('MAIN', 'No mobile searches found, stopping!') // Close mobile browser - return await browser.close() + return await this.closeBrowser(browser, account.email) } // Open a new tab to where the tasks are going to be completed @@ -235,11 +220,12 @@ export class MicrosoftRewardsBot { const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0] // If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid - if (mobileSearchPoints && ((mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0)) { - log('MAIN', 'Unable to complete mobile searches, bad User-Agent?, retrying...') + // Retry until all points are gathered when (retryMobileSearch is enabled) + if (this.config.searchSettings.retryMobileSearch && mobileSearchPoints && ((mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0)) { + log('MAIN', 'Unable to complete mobile searches, bad User-Agent? Retrying...') // Close mobile browser - await browser.close() + await this.closeBrowser(browser, account.email) // Retry await this.Mobile(account) @@ -254,10 +240,21 @@ export class MicrosoftRewardsBot { log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`) // Close mobile browser + return await this.closeBrowser(browser, account.email) + } + + private async closeBrowser(browser: BrowserContext, email: string) { + // Save cookies + const cookies = await browser.cookies() + await saveSessionData(this.config.sessionPath, email, this.isMobile, cookies) + + // Close browser await browser.close() } + } + const bot = new MicrosoftRewardsBot() // Initialize accounts first and then start the bot diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 1709041..03ed345 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -7,6 +7,7 @@ export interface Config { workers: Workers; searchSettings: SearchSettings; webhook: Webhook; + saveFingerprint: boolean; } export interface SearchSettings { @@ -14,6 +15,7 @@ export interface SearchSettings { scrollRandomResults: boolean; clickRandomResults: boolean; searchDelay: SearchDelay; + retryMobileSearch: boolean; } export interface SearchDelay { diff --git a/src/util/Load.ts b/src/util/Load.ts index a5399f3..0b148a2 100644 --- a/src/util/Load.ts +++ b/src/util/Load.ts @@ -1,6 +1,9 @@ +import { Cookie } from 'playwright' +import { BrowserFingerprintWithHeaders } from 'fingerprint-generator' import fs from 'fs' import path from 'path' + import { Account } from '../interface/Account' import { Config } from '../interface/Config' @@ -34,7 +37,37 @@ export function loadConfig(): Config { } } -export async function loadSesion(sessionPath: string, email: string): Promise { +export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, getFingerprint: boolean) { + try { + // Fetch cookie file + const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`) + + let cookies: Cookie[] = [] + if (fs.existsSync(cookieFile)) { + const cookiesData = await fs.promises.readFile(cookieFile, 'utf-8') + cookies = JSON.parse(cookiesData) + } + + // Fetch fingerprint file + const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`) + + let fingerprint!: BrowserFingerprintWithHeaders + if (getFingerprint && fs.existsSync(fingerprintFile)) { + const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8') + fingerprint = JSON.parse(fingerprintData) + } + + return { + cookies: cookies, + fingerprint: fingerprint + } + + } catch (error) { + throw new Error(error as string) + } +} + +export async function saveSessionData(sessionPath: string, email: string, isMobile: boolean, cookies: Cookie[]): Promise { try { // Fetch path const sessionDir = path.join(__dirname, '../browser/', sessionPath, email) @@ -44,6 +77,28 @@ export async function loadSesion(sessionPath: string, email: string): Promise { + try { + // Fetch path + const sessionDir = path.join(__dirname, '../browser/', sessionPath, email) + + // Create session dir + if (!fs.existsSync(sessionDir)) { + await fs.promises.mkdir(sessionDir, { recursive: true }) + } + + // Save fingerprint to a file + await fs.promises.writeFile(path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`), JSON.stringify(fingerpint)) + return sessionDir } catch (error) { throw new Error(error as string) diff --git a/src/util/UserAgent.ts b/src/util/UserAgent.ts index 910ed45..3a76235 100644 --- a/src/util/UserAgent.ts +++ b/src/util/UserAgent.ts @@ -81,7 +81,7 @@ export async function getEdgeVersions() { export function getSystemComponents(mobile: boolean): string { const osId: string = mobile ? 'Linux' : 'Windows NT 10.0' - const uaPlatform: string = mobile ? 'Android 13' : 'Win64; x64' + const uaPlatform: string = mobile ? `Android 1${Math.floor(Math.random() * 5)}` : 'Win64; x64' if (mobile) { return `${uaPlatform}; ${osId}; K`