From c802492f181d5589b512dea9fe221d16b4b8eb1e Mon Sep 17 00:00:00 2001 From: TheNetsky <56271887+TheNetsky@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:16:40 +0200 Subject: [PATCH] 1.0 --- .eslintrc.js | 36 ++ .gitignore | 4 + .vscode/launch.json | 20 + README.md | 4 +- package.json | 33 ++ src/Browser.ts | 40 ++ src/BrowserFunc.ts | 130 +++++ src/BrowserUtil.ts | 70 +++ src/accounts.example.json | 10 + src/config.json | 4 + src/functions/DailySet.ts | 54 +++ src/functions/Login.ts | 117 +++++ src/functions/MorePromotions.ts | 51 ++ src/functions/activities/Poll.ts | 31 ++ src/functions/activities/Quiz.ts | 119 +++++ src/functions/activities/Search.ts | 233 +++++++++ src/functions/activities/UrlReward.ts | 26 + src/index.ts | 82 ++++ src/interface/Account.ts | 4 + src/interface/DashboardData.ts | 673 ++++++++++++++++++++++++++ src/interface/GoogleDailyTrends.ts | 44 ++ src/interface/QuizData.ts | 50 ++ src/interface/UserAgentUtil.ts | 62 +++ src/util/Logger.ts | 18 + src/util/UserAgent.ts | 108 +++++ src/util/Utils.ts | 22 + tsconfig.json | 71 +++ 27 files changed, 2115 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 package.json create mode 100644 src/Browser.ts create mode 100644 src/BrowserFunc.ts create mode 100644 src/BrowserUtil.ts create mode 100644 src/accounts.example.json create mode 100644 src/config.json create mode 100644 src/functions/DailySet.ts create mode 100644 src/functions/Login.ts create mode 100644 src/functions/MorePromotions.ts create mode 100644 src/functions/activities/Poll.ts create mode 100644 src/functions/activities/Quiz.ts create mode 100644 src/functions/activities/Search.ts create mode 100644 src/functions/activities/UrlReward.ts create mode 100644 src/index.ts create mode 100644 src/interface/Account.ts create mode 100644 src/interface/DashboardData.ts create mode 100644 src/interface/GoogleDailyTrends.ts create mode 100644 src/interface/QuizData.ts create mode 100644 src/interface/UserAgentUtil.ts create mode 100644 src/util/Logger.ts create mode 100644 src/util/UserAgent.ts create mode 100644 src/util/Utils.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..f8b5e61 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,36 @@ +module.exports = { + 'env': { + 'es2021': true, + 'node': true + }, + 'extends': [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended' + ], + 'parser': '@typescript-eslint/parser', + 'parserOptions': { + 'ecmaVersion': 12, + 'sourceType': 'module' + }, + 'plugins': [ + '@typescript-eslint' + ], + 'rules': { + 'linebreak-style': [ + 'error', + 'unix' + ], + 'quotes': [ + 'error', + 'single' + ], + 'semi': [ + 'error', + 'never' + ], + 'comma-dangle': 'off', + '@typescript-eslint/comma-dangle': 'error', + 'prefer-arrow-callback': 'error' + // Add any other rules you want to enforce here + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d22a070 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +sessions/ +node_modules/ +package-lock.json +accounts.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1ddf16e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${file}", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index a6a9a93..09b5eeb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # Microsoft-Rewards-Script -Automated Microsoft Rewards script, however this time using TypeScriptt and Puppteer. +Automated Microsoft Rewards script, however this time using TypeScript and Puppteer. + +Under development, however mainly for personal use! \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ada66c8 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "microsoft-rewards-script", + "version": "1.0.0", + "description": "Automatically do tasks for Microsoft Rewards but in TS", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "node ./dist/index.js", + "dev": "ts-node ./src/index.ts" + }, + "keywords": [ + "Bing Rewards", + "Microsoft Rewards", + "Bot", + "TypeScript", + "Puppeteer" + ], + "author": "Netsky", + "license": "ISC", + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "typescript": "^5.2.2" + }, + "dependencies": { + "axios": "^1.5.0", + "eslint": "^8.49.0", + "eslint-plugin-modules-newline": "^0.0.6", + "puppeteer": "^21.2.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.ts b/src/Browser.ts new file mode 100644 index 0000000..7261479 --- /dev/null +++ b/src/Browser.ts @@ -0,0 +1,40 @@ +import puppeteer from 'puppeteer-extra' +import StealthPlugin from 'puppeteer-extra-plugin-stealth' + +import { getUserAgent } from './util/UserAgent' +import { loadSesion } from './BrowserFunc' + +puppeteer.use(StealthPlugin()) + +export async function Browser(email: string, headless = false) { + const userAgent = await getUserAgent(false) + + const browser = await puppeteer.launch({ + headless: headless, // Set to true for a headless browser + userDataDir: await loadSesion(email), + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + `--user-agent=${userAgent.userAgent}` + ] + }) + + return browser +} + +export async function mobileBrowser(email: string, headless = false) { + const userAgent = await getUserAgent(true) + + const browser = await puppeteer.launch({ + headless: headless, // Set to true for a headless browser, + userDataDir: await loadSesion(email), + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + `--user-agent=${userAgent.userAgent}`, + '--window-size=568,1024' + ] + }) + + return browser +} \ No newline at end of file diff --git a/src/BrowserFunc.ts b/src/BrowserFunc.ts new file mode 100644 index 0000000..c30b9cf --- /dev/null +++ b/src/BrowserFunc.ts @@ -0,0 +1,130 @@ +import { Page } from 'puppeteer' +import fs from 'fs' +import path from 'path' + +import { baseURL, sessionPath } from './config.json' +import { wait } from './util/Utils' +import { tryDismissAllMessages, tryDismissCookieBanner } from './BrowserUtil' +import { log } from './util/Logger' + +import { Counters, DashboardData } from './interface/DashboardData' +import { QuizData } from './interface/QuizData' + +export async function goHome(page: Page): Promise { + + try { + const targetUrl = 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) + + 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 !== targetUrl.hostname) { + await tryDismissAllMessages(page) + + await wait(2000) + await page.goto(baseURL) + } + + await wait(5000) + log('MAIN', 'Visited homepage successfully') + } + + } catch (error) { + console.error('An error occurred:', error) + } +} + +export async function getDashboardData(page: Page): Promise { + await page.reload({ waitUntil: 'networkidle2' }) + + 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 new Error('Script containing dashboard data not found') + } + }) + + // 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) + + if (match && match[1]) { + return JSON.parse(match[1]) + } else { + throw new Error('Dashboard data not found in the script') + } + }, scriptContent) + + return dashboardData +} + +export async function getSearchPoints(page: Page): Promise { + const dashboardData = await getDashboardData(page) + + return dashboardData.userStatus.counters +} + +export async function getQuizData(page: Page): Promise { + const scriptContent = await page.evaluate(() => { + const scripts = Array.from(document.querySelectorAll('script')) + const targetScript = scripts.find(script => script.innerText.includes('_w.rewardsQuizRenderInfo')) + + if (targetScript) { + return targetScript.innerText + } else { + throw new Error('Script containing quiz data not found') + } + }) + + const quizData = await page.evaluate(scriptContent => { + // Extract the dashboard object using regex + const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s + const match = regex.exec(scriptContent) + + if (match && match[1]) { + return JSON.parse(match[1]) + } else { + throw new Error('Dashboard data not found in the script') + } + }, scriptContent) + + return quizData +} + +export async function loadSesion(email: string): Promise { + const sessionDir = path.join(__dirname, sessionPath, email) + + 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) + } +} \ No newline at end of file diff --git a/src/BrowserUtil.ts b/src/BrowserUtil.ts new file mode 100644 index 0000000..2ad0f32 --- /dev/null +++ b/src/BrowserUtil.ts @@ -0,0 +1,70 @@ +import { Page } from 'puppeteer' +import { wait } from './util/Utils' + +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 + + for (const button of buttons) { + try { + const element = await page.waitForSelector(button.selector, { timeout: 1000 }) + if (element) { + await element.click() + result = true + } + + } catch (error) { + continue + } + } + + return result +} + +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) + } + } + + } 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') + + if (cookieBanner) { + await cookieBanner.click() + } + } catch (error) { + // Continue if element is not found or other error occurs + } +} + +export async function getLatestTab(page: Page) { + await wait(2000) + const browser = page.browser() + const pages = await browser.pages() + const newTab = pages[pages.length - 1] as Page + + return newTab +} \ No newline at end of file diff --git a/src/accounts.example.json b/src/accounts.example.json new file mode 100644 index 0000000..fde0b3d --- /dev/null +++ b/src/accounts.example.json @@ -0,0 +1,10 @@ +[ + { + "email": "email_1", + "password": "password_1" + }, + { + "email": "email_2", + "password": "password_2" + } +] \ No newline at end of file diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..1b7c003 --- /dev/null +++ b/src/config.json @@ -0,0 +1,4 @@ +{ + "baseURL" : "https://rewards.bing.com", + "sessionPath": "sessions" +} \ No newline at end of file diff --git a/src/functions/DailySet.ts b/src/functions/DailySet.ts new file mode 100644 index 0000000..ddb9906 --- /dev/null +++ b/src/functions/DailySet.ts @@ -0,0 +1,54 @@ +import { Page } from 'puppeteer' +import { DashboardData } from '../interface/DashboardData' +import { doPoll } from './activities/Poll' +import { getFormattedDate } from '../util/Utils' +import { doQuiz } from './activities/Quiz' +import { log } from '../util/Logger' +import { doUrlReward } from './activities/UrlReward' + + +export async function doDailySet(page: Page, data: DashboardData) { + const todayData = data.dailySetPromotions[getFormattedDate()] + + const activitiesUncompleted = todayData?.filter(x => !x.complete) ?? [] + + if (!activitiesUncompleted.length) { + log('DAILY-SET', 'All daily set items have already been completed') + return + } + + for (const activity of activitiesUncompleted) { + log('DAILY-SET', 'Started doing daily set items') + switch (activity.promotionType) { + // Quiz (Poll/Quiz) + case 'quiz': + + switch (activity.pointProgressMax) { + // Poll (Usually 10 points) + case 10: + log('ACTIVITY', 'Found daily activity type: Poll') + await doPoll(page, activity) + break + + // Quizzes are usually 30-40 points + default: + log('ACTIVITY', 'Found daily activity type: Quiz') + await doQuiz(page, activity) + break + } + break + + // UrlReward (Visit) + case 'urlreward': + log('ACTIVITY', 'Found daily activity type: UrlReward') + await doUrlReward(page, activity) + break + + default: + break + } + + } + + log('DAILY-SET', 'Daily set items have been completed') +} \ No newline at end of file diff --git a/src/functions/Login.ts b/src/functions/Login.ts new file mode 100644 index 0000000..548256a --- /dev/null +++ b/src/functions/Login.ts @@ -0,0 +1,117 @@ +import { Page } from 'puppeteer' +import readline from 'readline' + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +import { wait } from '../util/Utils' +import { tryDismissAllMessages, tryDismissBingCookieBanner } from '../BrowserUtil' +import { log } from '../util/Logger' + +export async function login(page: Page, email: string, password: string) { + + 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) { + await page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 }) + + await execLogin(page, email, password) + log('LOGIN', 'Logged into Microsoft successfully') + } else { + log('LOGIN', 'Already logged in') + } + + // Check if logged in to bing + await checkBingLogin(page) + + // We're done logging in + log('LOGIN', 'Logged in successfully') + + } catch (error) { + 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) + await page.click('#idSIButton9') + + } catch (error) { + 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') + + } 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) + } + + } catch (error) { + log('LOGIN-BING', 'An error occurred:' + error, 'error') + } +} + +async function 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/MorePromotions.ts b/src/functions/MorePromotions.ts new file mode 100644 index 0000000..907e936 --- /dev/null +++ b/src/functions/MorePromotions.ts @@ -0,0 +1,51 @@ +import { Page } from 'puppeteer' +import { DashboardData } from '../interface/DashboardData' +import { doPoll } from './activities/Poll' +import { doQuiz } from './activities/Quiz' +import { log } from '../util/Logger' +import { doUrlReward } from './activities/UrlReward' + + +export async function doMorePromotions(page: Page, data: DashboardData) { + const morePromotions = data.morePromotions + + const activitiesUncompleted = morePromotions?.filter(x => !x.complete) ?? [] + + if (!activitiesUncompleted.length) { + log('MORE-PROMOTIONS', 'All more promotion items have already been completed') + return + } + + for (const activity of activitiesUncompleted) { + + switch (activity.promotionType) { + // Quiz (Poll/Quiz) + case 'quiz': + + switch (activity.pointProgressMax) { + // Poll (Usually 10 points) + case 10: + log('ACTIVITY', 'Found promotion activity type: Poll') + await doPoll(page, activity) + break + + // Quizzes are usually 30-40 points + default: + log('ACTIVITY', 'Found promotion activity type: Quiz') + await doQuiz(page, activity) + break + } + break + + // UrlReward (Visit) + case 'urlreward': + log('ACTIVITY', 'Found promotion activity type: UrlReward') + await doUrlReward(page, activity) + break + + default: + break + } + + } +} \ No newline at end of file diff --git a/src/functions/activities/Poll.ts b/src/functions/activities/Poll.ts new file mode 100644 index 0000000..85df605 --- /dev/null +++ b/src/functions/activities/Poll.ts @@ -0,0 +1,31 @@ +import { Page } from 'puppeteer' +import { MorePromotion, PromotionalItem } from '../../interface/DashboardData' +import { getLatestTab } from '../../BrowserUtil' +import { log } from '../../util/Logger' +import { wait } from '../../util/Utils' + +export async function doPoll(page: Page, data: PromotionalItem | MorePromotion) { + log('POLL', 'Trying to complete poll') + + try { + const selector = `[data-bi-id="${data.offerId}"]` + + // Wait for page to load and click to load the quiz in a new tab + await page.waitForSelector(selector, { timeout: 5000 }) + await page.click(selector) + + const pollPage = await getLatestTab(page) + + const buttonId = `#btoption${Math.floor(Math.random() * 2)}` + + await pollPage.waitForSelector(buttonId) + await pollPage.click(buttonId) + + await wait(2000) + await pollPage.close() + + log('POLL', 'Completed the poll successfully') + } catch (error) { + 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 new file mode 100644 index 0000000..c262656 --- /dev/null +++ b/src/functions/activities/Quiz.ts @@ -0,0 +1,119 @@ +import { Page } from 'puppeteer' +import { MorePromotion, PromotionalItem } from '../../interface/DashboardData' +import { getQuizData } from '../../BrowserFunc' +import { wait } from '../../util/Utils' +import { getLatestTab } from '../../BrowserUtil' +import { log } from '../../util/Logger' + +export async function doQuiz(page: Page, data: PromotionalItem | MorePromotion) { + log('QUIZ', 'Trying to complete quiz') + + try { + const selector = `[data-bi-id="${data.offerId}"]` + + // Wait for page to load and click to load the quiz in a new tab + await page.waitForSelector(selector, { timeout: 5000 }) + await page.click(selector) + + const quizPage = await getLatestTab(page) + + // Check if the quiz has been started or not + const quizNotStarted = await quizPage.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) + if (quizNotStarted) { + await quizPage.click('#rqStartQuiz') + } else { + log('QUIZ', 'Quiz has already been started, trying to finish it') + } + + await wait(2000) + + const quizData = await getQuizData(quizPage) + + const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining + + // All questions + for (let question = 0; question < questionsRemaining; question++) { + + if (quizData.numberOfOptions === 8) { + const answers: string[] = [] + + for (let i = 0; i < quizData.numberOfOptions; i++) { + const answerSelector = await quizPage.waitForSelector(`#rqAnswerOption${i}`) + const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) + await wait(500) + + if (answerAttribute && answerAttribute.toLowerCase() === 'true') { + answers.push(`#rqAnswerOption${i}`) + } + } + + for (const answer of answers) { + // Click the answer on page + await quizPage.click(answer) + await wait(1500) + + const refreshSuccess = await waitForQuizRefresh(quizPage) + if (!refreshSuccess) { + await quizPage.close() + log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') + return + } + } + + // Other type quiz + } else if ([2, 3, 4].includes(quizData.numberOfOptions)) { + const correctOption = quizData.correctAnswer + + for (let i = 0; i < quizData.numberOfOptions; i++) { + + const answerSelector = await quizPage.waitForSelector(`#rqAnswerOption${i}`) + const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) + + if (dataOption === correctOption) { + // Click the answer on page + await quizPage.click(`#rqAnswerOption${i}`) + await wait(1500) + + const refreshSuccess = await waitForQuizRefresh(quizPage) + if (!refreshSuccess) { + await quizPage.close() + log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') + return + } + } + } + + } + + } + + // Done with + await quizPage.close() + log('QUIZ', 'Completed the quiz successfully') + } catch (error) { + const quizPage = await getLatestTab(page) + await quizPage.close() + log('QUIZ', 'An error occurred:' + error, 'error') + } + +} + +async function waitForQuizRefresh(page: Page) { + try { + await page.waitForSelector('#rqHeaderCredits', { timeout: 5000 }) + return true + } catch (error) { + log('QUIZ-REFRESH', 'An error occurred:' + error, 'error') + return false + } +} + +async function checkQuizCompleted(page: Page) { + try { + await page.waitForSelector('#quizCompleteContainer', { timeout: 1000 }) + return true + } catch (error) { + return false + } +} +checkQuizCompleted \ No newline at end of file diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts new file mode 100644 index 0000000..352d50d --- /dev/null +++ b/src/functions/activities/Search.ts @@ -0,0 +1,233 @@ +import { Page } from 'puppeteer' +import axios from 'axios' + +import { log } from '../../util/Logger' +import { shuffleArray, wait } from '../../util/Utils' +import { getSearchPoints } from '../../BrowserFunc' + +import { DashboardData, DashboardImpression } from '../../interface/DashboardData' +import { GoogleTrends } from '../../interface/GoogleDailyTrends' + +export async function doSearch(page: Page, data: DashboardData, mobile: boolean) { + const locale = await page.evaluate(() => { + return navigator.language + }) + + log('SEARCH-BING', 'Starting bing searches') + + const mobileData = data.userStatus.counters.mobileSearch[0] as DashboardImpression // Mobile searches + const edgeData = data.userStatus.counters.pcSearch[1] as DashboardImpression // Edge searches + const genericData = data.userStatus.counters.pcSearch[0] as DashboardImpression // Normal searches + + let missingPoints = mobile ? + (mobileData.pointProgressMax - mobileData.pointProgress) : + (edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress) + + if (missingPoints == 0) { + log('SEARCH-BING', `Bing searches for ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`) + return + } + + // Generate search queries + const googleSearchQueries = shuffleArray(await getGoogleTrends(locale, missingPoints)) + + // 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 20 this when not gaining any points, we're assuming it's stuck. + + // Loop over Google search queries + for (let i = 0; i < googleSearchQueries.length; i++) { + const query = googleSearchQueries[i] as string + + log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`) + + const newData = await bingSearch(page, searchPage, query) + + const newMobileData = newData.mobileSearch[0] as DashboardImpression // Mobile searches + const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches + const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches + + const newMissingPoints = mobile ? + (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 (maxLoop > 20) { + log('SEARCH-BING', 'Search didn\'t gain point for 20 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`) + + let i = 0 + while (missingPoints > 0) { + const query = googleSearchQueries[i++] as string + + // Get related search terms to the Google search queries + const relatedTerms = await getRelatedTerms(query) + 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) + + const newMobileData = newData.mobileSearch[0] as DashboardImpression // Mobile searches + const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches + const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches + + const newMissingPoints = mobile ? + (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 we satisfied the searches + if (missingPoints == 0) { + break + } + + // Try 5 more times + if (maxLoop > 5) { + log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn') + break + } + } + } + } + } + + log('SEARCH-BING', 'Completed searches') +} + +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, { timeout: 3000 }) + 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') // Delete the selected text + await searchPage.keyboard.up('Control') + await searchPage.keyboard.type(query) + await searchPage.keyboard.press('Enter') + + await wait(Math.floor(Math.random() * (20_000 - 10_000) + 1) + 10_000) + + return await getSearchPoints(page) + + } catch (error) { + if (i === 5) { + log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error') + return await getSearchPoints(page) + } + + log('SEARCH-BING', 'Search failed, retrying...', 'warn') + await wait(4000) + } + } + + log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error') + return await getSearchPoints(page) +} + +async function getGoogleTrends(locale: string, queryCount: number): Promise { + const queryTerms: string[] = [] + let i = 0 + + 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=US&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.title.query.toLowerCase()) + + for (const relatedTopic of topic.relatedQueries) { + queryTerms.push(relatedTopic.query.toLowerCase()) + } + } + + // Deduplicate the search terms + const uniqueSearchTerms = Array.from(new Set(queryTerms)) + queryTerms.length = 0 + queryTerms.push(...uniqueSearchTerms) + + } catch (error) { + log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error') + } + } + + return queryTerms.slice(0, queryCount) +} + +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' + } + } + + 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') + + return `${year}${month}${day}` +} \ No newline at end of file diff --git a/src/functions/activities/UrlReward.ts b/src/functions/activities/UrlReward.ts new file mode 100644 index 0000000..0089933 --- /dev/null +++ b/src/functions/activities/UrlReward.ts @@ -0,0 +1,26 @@ +import { Page } from 'puppeteer' +import { getLatestTab } from '../../BrowserUtil' +import { log } from '../../util/Logger' +import { PromotionalItem, MorePromotion } from '../../interface/DashboardData' + + +export async function doUrlReward(page: Page, data: PromotionalItem | MorePromotion) { + log('URL-REWARD', 'Trying to complete UrlReward') + + try { + const selector = `[data-bi-id="${data.offerId}"]` + + // Wait for page to load and click to load the url reward in a new tab + await page.waitForSelector(selector, { timeout: 5000 }) + await page.click(selector) + + // After waiting, close the page + const visitPage = await getLatestTab(page) + await visitPage.close() + + log('URL-REWARD', 'Completed the UrlReward successfully') + } catch (error) { + log('URL-REWARD', 'An error occurred:' + error, 'error') + } + +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b46ad0a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,82 @@ +import { Browser, mobileBrowser } from './Browser' +import { getDashboardData, goHome } from './BrowserFunc' +import { doDailySet } from './functions/DailySet' +import { login } from './functions/Login' +import { doMorePromotions } from './functions/MorePromotions' +import { doSearch } from './functions/activities/Search' +import { log } from './util/Logger' +import accounts from './accounts.json' + +import { Account } from './interface/Account' + +async function init() { + + log('MAIN', 'Bot started') + + for (const account of accounts) { + log('MAIN', `Started tasks for account ${account.email}`) + + // DailySet, MorePromotions and Desktop Searches + await Desktop(account) + + // Mobile Searches + await Mobile(account) + + log('MAIN', `Completed tasks for account ${account.email}`) + } + + // Clean exit + log('MAIN', 'Bot exited') + process.exit() +} + +// Desktop +async function Desktop(account: Account) { + const browser = await Browser(account.email) + const page = await browser.newPage() + + // Login into MS Rewards + await login(page, account.email, account.password) + + await goHome(page) + + const data = await getDashboardData(page) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Complete daily set + await doDailySet(page, data) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Complete more promotions + await doMorePromotions(page, data) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Do desktop searches + await doSearch(page, data, false) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Close desktop browser + await browser.close() +} + +async function Mobile(account: Account) { + const browser = await mobileBrowser(account.email) + const page = await browser.newPage() + + // Login into MS Rewards + await login(page, account.email, account.password) + + await goHome(page) + + const data = await getDashboardData(page) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Do mobile searches + await doSearch(page, data, true) + log('MAIN', `Current point count: ${data.userStatus.availablePoints}`) + + // Close mobile browser + await browser.close() +} + +init() \ No newline at end of file diff --git a/src/interface/Account.ts b/src/interface/Account.ts new file mode 100644 index 0000000..e7f3589 --- /dev/null +++ b/src/interface/Account.ts @@ -0,0 +1,4 @@ +export interface Account { + email: string; + password: string; +} \ No newline at end of file diff --git a/src/interface/DashboardData.ts b/src/interface/DashboardData.ts new file mode 100644 index 0000000..1a7a8ac --- /dev/null +++ b/src/interface/DashboardData.ts @@ -0,0 +1,673 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface DashboardData { + userStatus: UserStatus; + promotionalItem: PromotionalItem; + dailySetPromotions: { [key: string]: PromotionalItem[] }; + streakPromotion: StreakPromotion; + streakBonusPromotions: StreakBonusPromotion[]; + punchCards: any[]; + dashboardFlights: DashboardFlights; + morePromotions: MorePromotion[]; + suggestedRewards: AutoRedeemItem[]; + coachMarks: CoachMarks; + welcomeTour: WelcomeTour; + userInterests: UserInterests; + isVisualParityTest: boolean; + mbingFlight: null; + langCountryMismatchPromo: null; + machineTranslationPromo: MachineTranslationPromo; + autoRedeemItem: AutoRedeemItem; + userProfile: UserProfile; +} + +export interface AutoRedeemItem { + name: null | string; + price: number; + provider: null | string; + disabled: boolean; + category: string; + title: string; + variableGoalSpecificTitle: string; + smallImageUrl: string; + mediumImageUrl: string; + largeImageUrl: string; + largeShowcaseImageUrl: string; + description: Description; + showcase: boolean; + showcaseInAllCategory: boolean; + originalPrice: number; + discountedPrice: number; + popular: boolean; + isTestOnly: boolean; + groupId: string; + inGroup: boolean; + isDefaultItemInGroup: boolean; + groupTitle: string; + groupImageUrl: string; + groupShowcaseImageUrl: string; + instantWinGameId: string; + instantWinPlayAgainSku: string; + isLowInStock: boolean; + isOutOfStock: boolean; + getCodeMessage: string; + disableEmail: boolean; + stockMessage: string; + comingSoonFlag: boolean; + isGenericDonation: boolean; + isVariableRedemptionItem: boolean; + variableRedemptionItemCurrencySymbol: null; + variableRedemptionItemMin: number; + variableRedemptionItemMax: number; + variableItemConfigPointsToCurrencyConversionRatio: number; + isAutoRedeem: boolean; +} + +export interface Description { + itemGroupText: string; + smallText: string; + largeText: string; + legalText: string; + showcaseTitle: string; + showcaseDescription: string; +} + +export interface CoachMarks { + streaks: WelcomeTour; +} + +export interface WelcomeTour { + promotion: DashboardImpression; + slides: Slide[]; +} + +export interface DashboardImpression { + name: null | string; + priority: number; + attributes: { [key: string]: string } | null; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; + benefits?: Benefit[]; + supportedLevelKeys?: string[]; + supportedLevelTitles?: string[]; + supportedLevelTitlesMobile?: string[]; + activeLevel?: string; + isCodexAutoJoinUser?: boolean; +} + +export interface Benefit { + key: string; + text: string; + url: null | string; + helpText: null | string; + supportedLevels: SupportedLevels; +} + +export interface SupportedLevels { + level1?: string; + level2: string; + level2XBoxGold: string; +} + +export interface Slide { + slideType: null; + slideShowTourId: string; + id: number; + title: string; + subtitle: null; + subtitle1: null; + description: string; + description1: null; + imageTitle: null; + image2Title: null | string; + image3Title: null | string; + image4Title: null | string; + imageDescription: null; + image2Description: null | string; + image3Description: null | string; + image4Description: null | string; + imageUrl: null | string; + darkImageUrl: null; + image2Url: null | string; + image3Url: null | string; + image4Url: null | string; + layout: null | string; + actionButtonText: null | string; + actionButtonUrl: null | string; + foregroundImageUrl: null; + backLink: null; + nextLink: CloseLink; + closeLink: CloseLink; + footnote: null | string; + termsText: null; + termsUrl: null; + privacyText: null; + privacyUrl: null; + taggedItem: null | string; + slideVisited: boolean; + aboutPageLinkText: null; + aboutPageLink: null; + redeemLink: null; + rewardsLink: null; + quizLinks?: any[]; + quizCorrectAnswerTitle?: string; + quizWrongAnswerTitle?: string; + quizAnswerDescription?: string; +} + +export interface CloseLink { + text: null | string; + url: null | string; +} + +export interface PromotionalItem { + name: string; + priority: number; + attributes: PromotionalItemAttributes; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface PromotionalItemAttributes { + animated_icon: string; + bg_image: string; + complete: string; + daily_set_date?: string; + description: string; + destination: string; + icon: string; + image: string; + link_text: string; + max: string; + offerid: string; + progress: string; + sc_bg_image: string; + sc_bg_large_image: string; + small_image: string; + state: string; + title: string; + type: string; + give_eligible: string; + sc_title?: string; + sc_description?: string; + legal_text?: string; + legal_link_text?: string; + promotional?: string; +} + +export interface DashboardFlights { + dashboardbannernav: string; + togglegiveuser: string; + spotifyRedirect: string; + give_eligible: string; + destination: string; +} + +export interface MachineTranslationPromo { +} + +export interface MorePromotion { + name: string; + priority: number; + attributes: { [key: string]: string }; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface StreakBonusPromotion { + name: string; + priority: number; + attributes: StreakBonusPromotionAttributes; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface StreakBonusPromotionAttributes { + hidden: string; + type: string; + title: string; + description: string; + image: string; + animated_icon: string; + activity_progress: string; + activity_max: string; + bonus_earned: string; + break_description: string; + give_eligible: string; + destination: string; +} + +export interface StreakPromotion { + lastUpdatedDate: Date; + breakImageUrl: string; + lifetimeMaxValue: number; + bonusPointsEarned: number; + name: string; + priority: number; + attributes: StreakPromotionAttributes; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface StreakPromotionAttributes { + hidden: string; + type: string; + title: string; + image: string; + activity_progress: string; + last_updated: Date; + break_image: string; + lifetime_max: string; + bonus_points: string; + give_eligible: string; + destination: string; +} + +export interface UserInterests { + name: string; + priority: number; + attributes: UserInterestsAttributes; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface UserInterestsAttributes { + hidden: string; + give_eligible: string; + destination: string; +} + +export interface UserProfile { + ruid: string; + attributes: UserProfileAttributes; +} + +export interface UserProfileAttributes { + publisher: string; + publisher_upd: Date; + creative: string; + creative_upd: Date; + program: string; + program_upd: Date; + country: string; + country_upd: Date; + referrerhash: string; + referrerhash_upd: Date; + optout_upd: Date; + language: string; + language_upd: Date; + target: string; + target_upd: Date; + created: Date; + created_upd: Date; + epuid: string; + epuid_upd: Date; + waitlistattributes: string; + waitlistattributes_upd: Date; + serpbotscore: string; + serpbotscore_upd: Date; + iscashbackeligible: string; + give_user: string; +} + +export interface UserStatus { + levelInfo: LevelInfo; + availablePoints: number; + lifetimePoints: number; + lifetimePointsRedeemed: number; + ePuid: string; + redeemGoal: AutoRedeemItem; + counters: Counters; + lastOrder: LastOrder; + dashboardImpression: DashboardImpression; + referrerProgressInfo: ReferrerProgressInfo; + isGiveModeOn: boolean; + giveBalance: number; + firstTimeGiveModeOptIn: null; + giveOrganizationName: string; + lifetimeGivingPoints: number; + isRewardsUser: boolean; + isMuidTrialUser: boolean; +} + +export interface Counters { + pcSearch: DashboardImpression[]; + mobileSearch: DashboardImpression[]; + shopAndEarn: DashboardImpression[]; + activityAndQuiz: ActivityAndQuiz[]; + dailyPoint: DashboardImpression[]; +} + +export interface ActivityAndQuiz { + name: string; + priority: number; + attributes: ActivityAndQuizAttributes; + offerId: string; + complete: boolean; + counter: number; + activityProgress: number; + activityProgressMax: number; + pointProgressMax: number; + pointProgress: number; + promotionType: string; + promotionSubtype: string; + title: string; + extBannerTitle: string; + titleStyle: string; + theme: string; + description: string; + extBannerDescription: string; + descriptionStyle: string; + showcaseTitle: string; + showcaseDescription: string; + imageUrl: string; + dynamicImage: string; + smallImageUrl: string; + backgroundImageUrl: string; + showcaseBackgroundImageUrl: string; + showcaseBackgroundLargeImageUrl: string; + promotionBackgroundLeft: string; + promotionBackgroundRight: string; + iconUrl: string; + animatedIconUrl: string; + animatedLargeBackgroundImageUrl: string; + destinationUrl: string; + linkText: string; + hash: string; + activityType: string; + isRecurring: boolean; + isHidden: boolean; + isTestOnly: boolean; + isGiveEligible: boolean; + level: string; + slidesCount: number; + legalText: string; + legalLinkText: string; + deviceType: string; +} + +export interface ActivityAndQuizAttributes { + type: string; + title: string; + link_text: string; + description: string; + foreground_color: string; + image: string; + recurring: string; + destination: string; + 'classification.ShowProgress': string; + hidden: string; + give_eligible: string; +} + +export interface LastOrder { + id: null; + price: number; + status: null; + sku: null; + timestamp: Date; + catalogItem: null; +} + +export interface LevelInfo { + activeLevel: string; + activeLevelName: string; + progress: number; + progressMax: number; + levels: Level[]; + benefitsPromotion: DashboardImpression; +} + +export interface Level { + key: string; + active: boolean; + name: string; + tasks: CloseLink[]; + privileges: CloseLink[]; +} + +export interface ReferrerProgressInfo { + pointsEarned: number; + pointsMax: number; + isComplete: boolean; + promotions: any[]; +} diff --git a/src/interface/GoogleDailyTrends.ts b/src/interface/GoogleDailyTrends.ts new file mode 100644 index 0000000..e86fa50 --- /dev/null +++ b/src/interface/GoogleDailyTrends.ts @@ -0,0 +1,44 @@ +export interface GoogleTrends { + default: Default; +} + +export interface Default { + trendingSearchesDays: TrendingSearchesDay[]; + endDateForNextRequest: string; + rssFeedPageUrl: string; +} + +export interface TrendingSearchesDay { + date: string; + formattedDate: string; + trendingSearches: TrendingSearch[]; +} + +export interface TrendingSearch { + title: Title; + formattedTraffic: string; + relatedQueries: Title[]; + image: Image; + articles: Article[]; + shareUrl: string; +} + +export interface Article { + title: string; + timeAgo: string; + source: string; + image?: Image; + url: string; + snippet: string; +} + +export interface Image { + newsUrl: string; + source: string; + imageUrl: string; +} + +export interface Title { + query: string; + exploreLink: string; +} diff --git a/src/interface/QuizData.ts b/src/interface/QuizData.ts new file mode 100644 index 0000000..0c3665d --- /dev/null +++ b/src/interface/QuizData.ts @@ -0,0 +1,50 @@ +export interface QuizData { + offerId: string; + quizId: string; + quizCategory: string; + IsCurrentQuestionCompleted: boolean; + quizRenderSummaryPage: boolean; + resetQuiz: boolean; + userClickedOnHint: boolean; + isDemoEnabled: boolean; + correctAnswer: string; + isMultiChoiceQuizType: boolean; + isPutInOrderQuizType: boolean; + isListicleQuizType: boolean; + isWOTQuizType: boolean; + isBugsForRewardsQuizType: boolean; + currentQuestionNumber: number; + maxQuestions: number; + resetTrackingCounters: boolean; + showWelcomePanel: boolean; + isAjaxCall: boolean; + showHint: boolean; + numberOfOptions: number; + isMobile: boolean; + inRewardsMode: boolean; + enableDailySetWelcomePane: boolean; + enableDailySetNonWelcomePane: boolean; + isDailySetUrlOffer: boolean; + isDailySetFlightEnabled: boolean; + dailySetUrlOfferId: string; + earnedCredits: number; + maxCredits: number; + creditsPerQuestion: number; + userAlreadyClickedOptions: number; + hasUserClickedOnOption: boolean; + recentAnswerChoice: string; + sessionTimerSeconds: string; + isOverlayMinimized: number; + ScreenReaderMsgOnMove: string; + ScreenReaderMsgOnDrop: string; + IsPartialPointsEnabled: boolean; + PrioritizeUrlOverCookies: boolean; + UseNewReportActivityAPI: boolean; + CorrectlyAnsweredQuestionCount: number; + showJoinRewardsPage: boolean; + CorrectOptionAnswer_WOT: string; + WrongOptionAnswer_WOT: string; + enableSlideAnimation: boolean; + ariaLoggingEnabled: boolean; + UseQuestionIndexInActivityId: boolean; +} diff --git a/src/interface/UserAgentUtil.ts b/src/interface/UserAgentUtil.ts new file mode 100644 index 0000000..e5d024b --- /dev/null +++ b/src/interface/UserAgentUtil.ts @@ -0,0 +1,62 @@ +// Chrome Product Data +export interface ChromeVersion { + timestamp: Date; + channels: Channels; +} + +export interface Channels { + Stable: Beta; + Beta: Beta; + Dev: Beta; + Canary: Beta; +} + +export interface Beta { + channel: string; + version: string; + revision: string; +} + +// Edge Product Data +export interface EdgeVersion { + Product: string; + Releases: Release[]; +} + +export interface Release { + ReleaseId: number; + Platform: Platform; + Architecture: Architecture; + CVEs: string[]; + ProductVersion: string; + Artifacts: Artifact[]; + PublishedTime: Date; + ExpectedExpiryDate: Date; +} + +export enum Architecture { + Arm64 = 'arm64', + Universal = 'universal', + X64 = 'x64', + X86 = 'x86' +} + +export interface Artifact { + ArtifactName: string; + Location: string; + Hash: string; + HashAlgorithm: HashAlgorithm; + SizeInBytes: number; +} + +export enum HashAlgorithm { + Sha256 = 'SHA256' +} + +export enum Platform { + Android = 'Android', + IOS = 'iOS', + Linux = 'Linux', + MACOS = 'MacOS', + Windows = 'Windows' +} \ No newline at end of file diff --git a/src/util/Logger.ts b/src/util/Logger.ts new file mode 100644 index 0000000..347c7dc --- /dev/null +++ b/src/util/Logger.ts @@ -0,0 +1,18 @@ +export function log(title: string, message: string, type?: 'log' | 'warn' | 'error') { + const currentTime = new Date().toISOString() + + switch (type) { + case 'warn': + console.warn(`[${currentTime}] [WARN] [${title}] ${message}`) + break + + case 'error': + console.error(`[${currentTime}] [ERROR] [${title}] ${message}`) + break + + default: + console.log(`[${currentTime}] [LOG] [${title}] ${message}`) + break + } + +} \ No newline at end of file diff --git a/src/util/UserAgent.ts b/src/util/UserAgent.ts new file mode 100644 index 0000000..c59006b --- /dev/null +++ b/src/util/UserAgent.ts @@ -0,0 +1,108 @@ +import axios from 'axios' +import { log } from './Logger' +import { ChromeVersion, EdgeVersion } from '../interface/UserAgentUtil' + +export async function getUserAgent(mobile: boolean) { + const system = getSystemComponents(mobile) + const app = await getAppComponents(mobile) + + const uaTemplate = mobile ? + `Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Mobile Safari/537.36 EdgA/${app.edge_version}` : + `Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Safari/537.36 Edg/${app.edge_version}` + + const platformVersion = `${mobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0` + + const uaMetadata = { + mobile, + platform: mobile ? 'Android' : 'Windows', + fullVersionList: [ + { brand: 'Not/A)Brand', version: '99.0.0.0' }, + { brand: 'Microsoft Edge', version: app['edge_version'] }, + { brand: 'Chromium', version: app['chrome_version'] } + ], + brands: [ + { brand: 'Not/A)Brand', version: '99' }, + { brand: 'Microsoft Edge', version: app['edge_major_version'] }, + { brand: 'Chromium', version: app['chrome_major_version'] } + ], + platformVersion, + architecture: mobile ? '' : 'x86', + bitness: mobile ? '' : '64', + model: '' + } + + return { userAgent: uaTemplate, userAgentMetadata: uaMetadata } +} + +export async function getChromeVersion(): Promise { + try { + const request = { + url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json', + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + + const response = await axios(request) + const data: ChromeVersion = response.data + return data.channels.Stable.version + + } catch (error) { + throw log('USERAGENT-CHROME-VERSION', 'An error occurred:' + error, 'error') + } +} + +export async function getEdgeVersions() { + try { + const request = { + url: 'https://edgeupdates.microsoft.com/api/products', + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + + const response = await axios(request) + const data: EdgeVersion[] = response.data + const stable = data.find(x => x.Product == 'Stable') as EdgeVersion + return { + android: stable.Releases.find(x => x.Platform == 'Android')?.ProductVersion, + windows: stable.Releases.find(x => (x.Platform == 'Windows' && x.Architecture == 'x64'))?.ProductVersion + } + + + } catch (error) { + throw log('USERAGENT-EDGE-VERSION', 'An error occurred:' + error, 'error') + } +} + +export function getSystemComponents(mobile: boolean): string { + const osId: string = mobile ? 'Linux' : 'Windows NT 10.0' + const uaPlatform: string = mobile ? 'Android 10' : 'Win64; x64' + + if (mobile) { + return `${uaPlatform}; ${osId}; K` + } + + return `${uaPlatform}; ${osId}` +} + + +export async function getAppComponents(mobile: boolean) { + const versions = await getEdgeVersions() + const edgeVersion = mobile ? versions.android : versions.windows as string + const edgeMajorVersion = edgeVersion?.split('.')[0] + + const chromeVersion = await getChromeVersion() + const chromeMajorVersion = chromeVersion?.split('.')[0] + const chromeReducedVersion = `${chromeMajorVersion}.0.0.0` + + return { + edge_version: edgeVersion as string, + edge_major_version: edgeMajorVersion as string, + chrome_version: chromeVersion as string, + chrome_major_version: chromeMajorVersion as string, + chrome_reduced_version: chromeReducedVersion as string + } +} \ No newline at end of file diff --git a/src/util/Utils.ts b/src/util/Utils.ts new file mode 100644 index 0000000..7477413 --- /dev/null +++ b/src/util/Utils.ts @@ -0,0 +1,22 @@ +export async function wait(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +export function getFormattedDate(ms = Date.now()) { + 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: string[]): string[] { + const shuffledArray = array.slice() + + shuffledArray.sort(() => Math.random() - 0.5) + + return shuffledArray +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..87a17ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file