12 Commits

Author SHA1 Message Date
TheNetsky
9ea7f5c452 1.4.0
- Switched from Puppeteer to Playwright
- Fixed mobile searches not working
- Added fingerprint saving in config
- Added mobile search retry in config
2024-01-07 16:33:48 +01:00
TheNetsky
f3fb641ecd 1.3.2
- Better generation of mobile user agents
2023-12-27 11:38:52 +01:00
TheNetsky
bca1e7c896 1.3.1
- Poll clicking will be attempted regardless of the selector being found
- Fix tabs sticking upon search error
- Updated dependencies
2023-12-20 13:03:08 +01:00
Netsky
fd7c8e36d4 Update README.md 2023-12-01 12:12:26 +01:00
Netsky
dcb0c25d46 Remove the feature of Promo Items 2023-12-01 12:02:17 +01:00
TheNetsky
a8cb5482d4 1.3.0 2023-12-01 11:49:14 +01:00
TheNetsky
28286ff9fe Pre 1.3.0 2023-11-23 14:38:58 +01:00
TheNetsky
1d6167aeca Missing await 2023-11-04 16:24:25 +01:00
TheNetsky
3b15fe19a7 1.2.5 2023-11-04 16:21:56 +01:00
TheNetsky
66dcdd4a7a 1.2.4
Disable "This or That" quiz.
2023-10-30 21:10:47 +01:00
TheNetsky
40f32af67f 1.2.3 2023-10-30 20:37:40 +01:00
TheNetsky
c2b68faa74 1.2.2 2023-10-26 15:31:35 +02:00
25 changed files with 1459 additions and 1098 deletions

20
.vscode/launch.json vendored
View File

@@ -1,20 +0,0 @@
{
// 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": [
"<node_internals>/**"
],
"program": "${file}",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}

View File

@@ -1,5 +1,5 @@
# Microsoft-Rewards-Script # 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! Under development, however mainly for personal use!
@@ -11,6 +11,11 @@ Under development, however mainly for personal use!
5. Run `npm run build` to build the script 5. Run `npm run build` to build the script
6. Run `npm run start` to start the built script 6. Run `npm run start` to start the built script
## Notes ##
- If you end the script without closing the browser window first (only with headless as false), you'll be left with hanging chrome instances using resources. Use taskmanager to kill these or use the included `npm run chrome-kill-win` script. (Windows)
- If you automate this script, set it to run at least 2 times a day to make sure it picked up all tasks, set `"runOnZeroPoints": false` so it doesn't run when no points are found.
## Features ## ## Features ##
- [x] Multi-Account Support - [x] Multi-Account Support
- [x] Session Storing - [x] Session Storing
@@ -31,8 +36,7 @@ Under development, however mainly for personal use!
- [x] Completing Click Rewards - [x] Completing Click Rewards
- [x] Completing Polls - [x] Completing Polls
- [x] Completing Punchcards - [x] Completing Punchcards
- [ ] Solving This Or That Quiz - [x] Solving This Or That Quiz (Random)
- [x] Clicking Promotional Items
- [x] Solving ABC Quiz - [x] Solving ABC Quiz
- [ ] Completing Shopping Game - [ ] Completing Shopping Game
- [ ] Completing Gaming Tab - [ ] Completing Gaming Tab

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "1.2.1", "version": "1.4.0",
"description": "Automatically do tasks for Microsoft Rewards but in TS!", "description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@@ -10,7 +10,8 @@
"build": "tsc", "build": "tsc",
"start": "node ./dist/index.js", "start": "node ./dist/index.js",
"ts-start": "ts-node ./src/index.ts", "ts-start": "ts-node ./src/index.ts",
"dev": "ts-node ./src/index.ts -dev" "dev": "ts-node ./src/index.ts -dev",
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\""
}, },
"keywords": [ "keywords": [
"Bing Rewards", "Bing Rewards",
@@ -18,23 +19,23 @@
"Bot", "Bot",
"Script", "Script",
"TypeScript", "TypeScript",
"Puppeteer", "Playwright",
"Cheerio" "Cheerio"
], ],
"author": "Netsky", "author": "Netsky",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/eslint-plugin": "^6.7.0",
"typescript": "^5.2.2" "eslint": "^8.54.0",
"eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.3.3"
}, },
"dependencies": { "dependencies": {
"axios": "^1.5.1", "axios": "^1.6.2",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"eslint": "^8.49.0", "fingerprint-generator": "^2.1.46",
"eslint-plugin-modules-newline": "^0.0.6", "fingerprint-injector": "^2.1.46",
"puppeteer": "^21.2.1", "playwright": "^1.40.1",
"puppeteer-extra": "^3.3.6", "ts-node": "^10.9.2"
"puppeteer-extra-plugin-stealth": "^2.11.2",
"ts-node": "^10.9.1"
} }
} }

View File

@@ -1,34 +1,67 @@
import puppeteer from 'puppeteer-extra' import playwright from 'playwright'
import stealthPlugin from 'puppeteer-extra-plugin-stealth' import { BrowserContext } from 'playwright'
import { getUserAgent } from '../util/UserAgent' import { newInjectedContext } from 'fingerprint-injector'
import { loadSesion } from './BrowserFunc' import { FingerprintGenerator } from 'fingerprint-generator'
import { MicrosoftRewardsBot } from '../index'
import { loadSessionData, saveFingerprintData } from '../util/Load'
import { AccountProxy } from '../interface/Account' import { AccountProxy } from '../interface/Account'
import { headless } from '../config.json' /* Test Stuff
https://abrahamjuliot.github.io/creepjs/
puppeteer.use(stealthPlugin()) https://botcheck.luminati.io/
http://f.vision/
*/
class Browser { class Browser {
private bot: MicrosoftRewardsBot
async createBrowser(email: string, proxy: AccountProxy, isMobile: boolean) { constructor(bot: MicrosoftRewardsBot) {
const userAgent = await getUserAgent(isMobile) this.bot = bot
}
const browser = await puppeteer.launch({ async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
headless: headless, const browser = await playwright.chromium.launch({
userDataDir: await loadSesion(email), //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: [ args: [
'--no-sandbox', '--no-sandbox',
'--mute-audio', '--mute-audio',
'--disable-setuid-sandbox', '--disable-setuid-sandbox',
`--user-agent=${userAgent.userAgent}`, '--ignore-certificate-errors',
isMobile ? '--window-size=568,1024' : '', '--ignore-certificate-errors-spki-list',
proxy.url ? `--proxy-server=${proxy.url}:${proxy.port}` : '' '--ignore-ssl-errors'
] ]
}) })
return browser 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']
})
return fingerPrintData
} }
} }

View File

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

View File

@@ -1,9 +1,16 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { wait } from './../util/Utils' import { MicrosoftRewardsBot } from '../index'
import { log } from './../util/Logger'
export async function tryDismissAllMessages(page: Page): Promise<boolean> {
export default class BrowserUtil {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async tryDismissAllMessages(page: Page): Promise<boolean> {
const buttons = [ const buttons = [
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' }, { selector: '#iLandingViewAction', label: 'iLandingViewAction' },
{ selector: '#iShowSkip', label: 'iShowSkip' }, { selector: '#iShowSkip', label: 'iShowSkip' },
@@ -17,7 +24,7 @@ export async function tryDismissAllMessages(page: Page): Promise<boolean> {
for (const button of buttons) { for (const button of buttons) {
try { 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) { if (element) {
await element.click() await element.click()
result = true result = true
@@ -31,7 +38,7 @@ export async function tryDismissAllMessages(page: Page): Promise<boolean> {
return result return result
} }
export async function tryDismissCookieBanner(page: Page): Promise<void> { async tryDismissCookieBanner(page: Page): Promise<void> {
try { try {
await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 }) await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 })
const cookieBanner = await page.$('#cookieConsentContainer') const cookieBanner = await page.$('#cookieConsentContainer')
@@ -40,7 +47,7 @@ export async function tryDismissCookieBanner(page: Page): Promise<void> {
const button = await cookieBanner.$('button') const button = await cookieBanner.$('button')
if (button) { if (button) {
await button.click() await button.click()
await wait(2000) await this.bot.utils.wait(2000)
} }
} }
@@ -49,7 +56,7 @@ export async function tryDismissCookieBanner(page: Page): Promise<void> {
} }
} }
export async function tryDismissBingCookieBanner(page: Page): Promise<void> { async tryDismissBingCookieBanner(page: Page): Promise<void> {
try { try {
await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 }) await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 })
const cookieBanner = await page.$('#bnp_btn_accept') const cookieBanner = await page.$('#bnp_btn_accept')
@@ -62,20 +69,56 @@ export async function tryDismissBingCookieBanner(page: Page): Promise<void> {
} }
} }
export async function getLatestTab(page: Page): Promise<Page> { async getLatestTab(page: Page): Promise<Page> {
try { try {
await wait(500) await this.bot.utils.wait(500)
const browser = page.browser() const browser = page.context()
const pages = await browser.pages() const pages = browser.pages()
const newTab = pages[pages.length - 1] const newTab = pages[pages.length - 1]
if (newTab) { if (newTab) {
return newTab return newTab
} }
throw log('GET-NEW-TAB', 'Unable to get latest tab', 'error') throw this.bot.log('GET-NEW-TAB', 'Unable to get latest tab', 'error')
} catch (error) { } catch (error) {
throw log('GET-NEW-TAB', 'An error occurred:' + error, 'error') throw this.bot.log('GET-NEW-TAB', 'An error occurred:' + error, 'error')
} }
} }
async getTabs(page: Page) {
try {
const browser = page.context()
const pages = browser.pages()
const homeTab = pages[1]
let homeTabURL: URL
if (!homeTab) {
throw this.bot.log('GET-TABS', 'Home tab could not be found!', 'error')
} else {
homeTabURL = new URL(homeTab.url())
if (homeTabURL.hostname !== 'rewards.bing.com') {
throw this.bot.log('GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
}
}
const workerTab = pages[2]
if (!workerTab) {
throw this.bot.log('GET-TABS', 'Worker tab could not be found!', 'error')
}
return {
homeTab: homeTab,
workerTab: workerTab
}
} catch (error) {
throw this.bot.log('GET-TABS', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -4,6 +4,7 @@
"headless": false, "headless": false,
"runOnZeroPoints": true, "runOnZeroPoints": true,
"clusters": 1, "clusters": 1,
"saveFingerprint": false,
"workers": { "workers": {
"doDailySet": true, "doDailySet": true,
"doMorePromotions": true, "doMorePromotions": true,
@@ -14,7 +15,12 @@
"searchSettings": { "searchSettings": {
"useGeoLocaleQueries": false, "useGeoLocaleQueries": false,
"scrollRandomResults": true, "scrollRandomResults": true,
"clickRandomResults": true "clickRandomResults": true,
"searchDelay": {
"min": 10000,
"max": 20000
},
"retryMobileSearch": true
}, },
"webhook": { "webhook": {
"enabled": false, "enabled": false,

View File

@@ -0,0 +1,52 @@
import { Page } from 'playwright'
import { MicrosoftRewardsBot } from '../index'
import { Search } from './activities/Search'
import { ABC } from './activities/ABC'
import { Poll } from './activities/Poll'
import { Quiz } from './activities/Quiz'
import { ThisOrThat } from './activities/ThisOrThat'
import { UrlReward } from './activities/UrlReward'
import { DashboardData } from '../interface/DashboardData'
export default class Activities {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
const search = new Search(this.bot)
await search.doSearch(page, data)
}
doABC = async (page: Page): Promise<void> => {
const abc = new ABC(this.bot)
await abc.doABC(page)
}
doPoll = async (page: Page): Promise<void> => {
const poll = new Poll(this.bot)
await poll.doPoll(page)
}
doThisOrThat = async (page: Page): Promise<void> => {
const thisOrThat = new ThisOrThat(this.bot)
await thisOrThat.doThisOrThat(page)
}
doQuiz = async (page: Page): Promise<void> => {
const quiz = new Quiz(this.bot)
await quiz.doQuiz(page)
}
doUrlReward = async (page: Page): Promise<void> => {
const urlReward = new UrlReward(this.bot)
await urlReward.doUrlReward(page)
}
}

View File

@@ -1,66 +1,75 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import readline from 'readline' import readline from 'readline'
import { MicrosoftRewardsBot } from '../index'
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}) })
import { tryDismissAllMessages, tryDismissBingCookieBanner } from '../browser/BrowserUtil'
import { wait } from '../util/Utils'
import { log } from '../util/Logger'
export async function login(page: Page, email: string, password: string) { export class Login {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async login(page: Page, email: string, password: string) {
try { try {
// Navigate to the Bing login page // Navigate to the Bing login page
await page.goto('https://login.live.com/') await page.goto('https://rewards.bing.com/signin')
const isLoggedIn = await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 5000 }).then(() => true).catch(() => false) const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
if (!isLoggedIn) { if (!isLoggedIn) {
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 5000 }).then(() => true).catch(() => false) // Check if account is locked
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 10_000 }).then(() => true).catch(() => false)
if (isLocked) { if (isLocked) {
log('LOGIN', 'This account has been locked!', 'error') this.bot.log('LOGIN', 'This account has been locked!', 'error')
throw new Error('Account has been locked!') 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 execLogin(page, email, password) await this.execLogin(page, email, password)
log('LOGIN', 'Logged into Microsoft successfully') this.bot.log('LOGIN', 'Logged into Microsoft successfully')
} else { } else {
log('LOGIN', 'Already logged in') this.bot.log('LOGIN', 'Already logged in')
} }
// Check if logged in to bing // Check if logged in to bing
await checkBingLogin(page) await this.checkBingLogin(page)
// We're done logging in // We're done logging in
log('LOGIN', 'Logged in successfully') this.bot.log('LOGIN', 'Logged in successfully')
} catch (error) { } catch (error) {
// Throw and don't continue // Throw and don't continue
throw log('LOGIN', 'An error occurred:' + error, 'error') throw this.bot.log('LOGIN', 'An error occurred:' + error, 'error')
} }
} }
async function execLogin(page: Page, email: string, password: string) { private async execLogin(page: Page, email: string, password: string) {
await page.type('#i0116', email) await page.type('#i0116', email)
await page.click('#idSIButton9') await page.click('#idSIButton9')
log('LOGIN', 'Email entered successfully') this.bot.log('LOGIN', 'Email entered successfully')
try { try {
await page.waitForSelector('#i0118', { visible: true, timeout: 2000 }) await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
await wait(2000) await this.bot.utils.wait(2000)
await page.type('#i0118', password) await page.type('#i0118', password)
await page.click('#idSIButton9') await page.click('#idSIButton9')
// When erroring at this stage it means a 2FA code is required
} catch (error) { } catch (error) {
log('LOGIN', '2FA code required') this.bot.log('LOGIN', '2FA code required')
// Wait for user input
const code = await new Promise<string>((resolve) => { const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => { rl.question('Enter 2FA code:\n', (input) => {
rl.close() rl.close()
@@ -72,23 +81,23 @@ async function execLogin(page: Page, email: string, password: string) {
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
} finally { } finally {
log('LOGIN', 'Password entered successfully') this.bot.log('LOGIN', 'Password entered successfully')
} }
const currentURL = new URL(page.url()) const currentURL = new URL(page.url())
while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') { while (currentURL.pathname !== '/' || currentURL.hostname !== 'rewards.bing.com') {
await tryDismissAllMessages(page) await this.bot.browser.utils.tryDismissAllMessages(page)
currentURL.href = page.url() currentURL.href = page.url()
} }
// Wait for login to complete // Wait for login to complete
await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 }) await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 })
} }
async function checkBingLogin(page: Page): Promise<void> { private async checkBingLogin(page: Page): Promise<void> {
try { try {
log('LOGIN-BING', 'Verifying Bing login') this.bot.log('LOGIN-BING', 'Verifying Bing login')
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F') 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 const maxIterations = 5
@@ -97,25 +106,25 @@ async function checkBingLogin(page: Page): Promise<void> {
const currentUrl = new URL(page.url()) const currentUrl = new URL(page.url())
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') { if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
await wait(3000) await this.bot.browser.utils.tryDismissBingCookieBanner(page)
await tryDismissBingCookieBanner(page)
const loggedIn = await checkBingLoginStatus(page) const loggedIn = await this.checkBingLoginStatus(page)
if (loggedIn) { // If mobile browser, skip this step
log('LOGIN-BING', 'Bing login verification passed!') if (loggedIn || this.bot.isMobile) {
this.bot.log('LOGIN-BING', 'Bing login verification passed!')
break break
} }
} }
await wait(1000) await this.bot.utils.wait(1000)
} }
} catch (error) { } catch (error) {
log('LOGIN-BING', 'An error occurred:' + error, 'error') this.bot.log('LOGIN-BING', 'An error occurred:' + error, 'error')
} }
} }
async function checkBingLoginStatus(page: Page): Promise<boolean> { private async checkBingLoginStatus(page: Page): Promise<boolean> {
try { try {
await page.waitForSelector('#id_n', { timeout: 5000 }) await page.waitForSelector('#id_n', { timeout: 5000 })
return true return true
@@ -123,3 +132,5 @@ async function checkBingLoginStatus(page: Page): Promise<boolean> {
return false return false
} }
} }
}

View File

@@ -1,76 +1,85 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { doPoll } from './activities/Poll'
import { doQuiz } from './activities/Quiz'
import { doUrlReward } from './activities/UrlReward'
import { doThisOrThat } from './activities/ThisOrThat'
import { doABC } from './activities/ABC'
import { getPunchCardActivity } from '../browser/BrowserFunc'
import { getLatestTab } from '../browser/BrowserUtil'
import { getFormattedDate, wait } from '../util/Utils'
import { log } from '../util/Logger'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData' import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
import { baseURL } from '../config.json' import { MicrosoftRewardsBot } from '../index'
export class Workers {
public bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
// Daily Set // Daily Set
export async function doDailySet(page: Page, data: DashboardData) { async doDailySet(page: Page, data: DashboardData) {
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
const todayData = data.dailySetPromotions[getFormattedDate()]
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
log('DAILY-SET', 'All Daily Set" items have already been completed') this.bot.log('DAILY-SET', 'All Daily Set" items have already been completed')
return return
} }
// Solve Activities // Solve Activities
log('DAILY-SET', 'Started solving "Daily Set" items') this.bot.log('DAILY-SET', 'Started solving "Daily Set" items')
await solveActivities(page, activitiesUncompleted) await this.solveActivities(page, activitiesUncompleted)
log('DAILY-SET', 'All "Daily Set" items have been completed') page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log('DAILY-SET', 'All "Daily Set" items have been completed')
} }
// Punch Card // Punch Card
export async function doPunchCard(page: Page, data: DashboardData) { async doPunchCard(page: Page, data: DashboardData) {
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
if (!punchCardsUncompleted.length) { if (!punchCardsUncompleted.length) {
log('PUNCH-CARD', 'All "Punch Cards" have already been completed') this.bot.log('PUNCH-CARD', 'All "Punch Cards" have already been completed')
return return
} }
for (const punchCard of punchCardsUncompleted) { for (const punchCard of punchCardsUncompleted) {
// Get latest page for each card
page = await this.bot.browser.utils.getLatestTab(page)
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
// Solve Activities // Solve Activities
log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`) this.bot.log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
const browser = page.browser()
page = await browser.newPage()
// Got to punch card index page in a new tab // Got to punch card index page in a new tab
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: baseURL }) await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
await solveActivities(page, activitiesUncompleted, punchCard) // Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { })
// Close the punch card index page await this.solveActivities(page, activitiesUncompleted, punchCard)
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close() await page.close()
} else {
log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`) await this.bot.browser.func.goHome(page)
} }
log('PUNCH-CARD', 'All "Punch Card" items have been completed') this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
}
this.bot.log('PUNCH-CARD', 'All "Punch Card" items have been completed')
} }
// More Promotions // More Promotions
export async function doMorePromotions(page: Page, data: DashboardData) { async doMorePromotions(page: Page, data: DashboardData) {
const morePromotions = data.morePromotions const morePromotions = data.morePromotions
// Check if there is a promotional item // Check if there is a promotional item
@@ -81,45 +90,70 @@ export async function doMorePromotions(page: Page, data: DashboardData) {
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [] const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
return return
} }
// Solve Activities // Solve Activities
log('MORE-PROMOTIONS', 'Started solving "More Promotions" item') this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" item')
await solveActivities(page, activitiesUncompleted) page = await this.bot.browser.utils.getLatestTab(page)
log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed') await this.solveActivities(page, activitiesUncompleted)
page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
} }
// Solve all the different types of activities // Solve all the different types of activities
async function solveActivities(page: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) { private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
for (const activity of activities) { for (const activity of activities) {
try { try {
// Reselect the worker page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
const pages = activityPage.context().pages()
if (pages.length > 3) {
await activityPage.close()
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
}
await this.bot.utils.wait(1000)
if (activityPage.url() !== activityInitial) {
await activityPage.goto(activityInitial)
}
let selector = `[data-bi-id="${activity.offerId}"]` let selector = `[data-bi-id="${activity.offerId}"]`
if (punchCard) { if (punchCard) {
selector = await getPunchCardActivity(page, activity) selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} else if (activity.name.toLowerCase().includes('membercenter')) { } else if (activity.name.toLowerCase().includes('membercenter')) {
// Promotion
if (activity.priority === 1) {
selector = '#promo-item'
} else {
selector = `[data-bi-id="${activity.name}"]` selector = `[data-bi-id="${activity.name}"]`
} }
}
// Wait for element to load
await page.waitForSelector(selector, { timeout: 5000 })
// Click element, it will be opened in a new tab // Click element, it will be opened in a new tab
await page.click(selector) await activityPage.click(selector)
// Select the new activity page // Select the new activity page
const activityPage = await getLatestTab(page) activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Wait for the new tab to fully load, ignore error.
/*
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.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { })
await this.bot.utils.wait(5000)
switch (activity.promotionType) { switch (activity.promotionType) {
// Quiz (Poll, Quiz or ABC) // Quiz (Poll, Quiz or ABC)
@@ -129,45 +163,49 @@ async function solveActivities(page: Page, activities: PromotionalItem[] | MoreP
case 10: case 10:
// Normal poll // Normal poll
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) { if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
await doPoll(activityPage) await this.bot.activities.doPoll(activityPage)
} else { // ABC } else { // ABC
log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
await doABC(activityPage) await this.bot.activities.doABC(activityPage)
} }
break break
// This Or That Quiz (Usually 50 points) // This Or That Quiz (Usually 50 points)
case 50: case 50:
log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
await doThisOrThat(activityPage) await this.bot.activities.doThisOrThat(activityPage)
break break
// Quizzes are usually 30-40 points // Quizzes are usually 30-40 points
default: default:
log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
await doQuiz(activityPage) await this.bot.activities.doQuiz(activityPage)
break break
} }
break break
// UrlReward (Visit) // UrlReward (Visit)
case 'urlreward': case 'urlreward':
log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
await doUrlReward(activityPage) await this.bot.activities.doUrlReward(activityPage)
break break
// Misc, Usually UrlReward Type // Misc, Usually UrlReward Type
default: default:
log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`) this.bot.log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`)
await doUrlReward(activityPage) await this.bot.activities.doUrlReward(activityPage)
break break
} }
// Cooldown // Cooldown
await wait(1500) await this.bot.utils.wait(2000)
} catch (error) { } catch (error) {
log('ACTIVITY', 'An error occurred:' + error, 'error') this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error')
}
} }
} }
} }

View File

@@ -1,52 +1,52 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { refreshCheerio } from '../../browser/BrowserFunc' import { Workers } from '../Workers'
import { getLatestTab } from '../../browser/BrowserUtil'
import { log } from '../../util/Logger'
import { randomNumber, wait } from '../../util/Utils'
export async function doABC(page: Page) {
log('ABC', 'Trying to complete poll') export class ABC extends Workers {
async doABC(page: Page) {
this.bot.log('ABC', 'Trying to complete poll')
try { try {
await wait(2000) let $ = await this.bot.browser.func.refreshCheerio(page)
let $ = await refreshCheerio(page)
// Don't loop more than 15 in case unable to solve, would lock otherwise // Don't loop more than 15 in case unable to solve, would lock otherwise
const maxIterations = 15 const maxIterations = 15
let i let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 5000 }) await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 })
const answers = $('.wk_OptionClickClass') const answers = $('.wk_OptionClickClass')
const answer = answers[randomNumber(0, 2)]?.attribs['id'] const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { visible: true, timeout: 5000 }) await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 })
await wait(2000) await this.bot.utils.wait(2000)
await page.click(`#${answer}`) // Click answer await page.click(`#${answer}`) // Click answer
await wait(4000) await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { visible: true, timeout: 5000 }) await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 })
await page.click('div.wk_button') // Click next question button await page.click('div.wk_button') // Click next question button
page = await getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)
$ = await refreshCheerio(page) $ = await this.bot.browser.func.refreshCheerio(page)
await wait(1000) await this.bot.utils.wait(1000)
} }
await wait(4000) await this.bot.utils.wait(4000)
await page.close() await page.close()
if (i === maxIterations) { if (i === maxIterations) {
log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn') this.bot.log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
} else { } else {
log('ABC', 'Completed the ABC successfully') this.bot.log('ABC', 'Completed the ABC successfully')
} }
} catch (error) { } catch (error) {
await page.close() await page.close()
log('ABC', 'An error occurred:' + error, 'error') this.bot.log('ABC', 'An error occurred:' + error, 'error')
} }
} }
}

View File

@@ -1,26 +1,29 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { log } from '../../util/Logger' import { Workers } from '../Workers'
import { randomNumber, wait } from '../../util/Utils'
export async function doPoll(page: Page) {
log('POLL', 'Trying to complete poll') export class Poll extends Workers {
async doPoll(page: Page) {
this.bot.log('POLL', 'Trying to complete poll')
try { try {
const buttonId = `#btoption${Math.floor(randomNumber(0, 1))}` const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForNetworkIdle({ timeout: 5000 }) await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not
await page.waitForSelector(buttonId, { visible: true, timeout: 5000 }) await this.bot.utils.wait(2000)
await wait(2000)
await page.click(buttonId) await page.click(buttonId)
await wait(4000) await this.bot.utils.wait(4000)
await page.close() await page.close()
log('POLL', 'Completed the poll successfully') this.bot.log('POLL', 'Completed the poll successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
log('POLL', 'An error occurred:' + error, 'error') this.bot.log('POLL', 'An error occurred:' + error, 'error')
} }
} }
}

View File

@@ -1,27 +1,25 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { getQuizData, waitForQuizRefresh } from '../../browser/BrowserFunc' import { Workers } from '../Workers'
import { wait } from '../../util/Utils'
import { log } from '../../util/Logger'
export async function doQuiz(page: Page) {
log('QUIZ', 'Trying to complete quiz') export class Quiz extends Workers {
async doQuiz(page: Page) {
this.bot.log('QUIZ', 'Trying to complete quiz')
try { try {
await page.waitForNetworkIdle({ timeout: 5000 })
await wait(2000)
// Check if the quiz has been started or not // Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (quizNotStarted) { if (quizNotStarted) {
await page.click('#rqStartQuiz') await page.click('#rqStartQuiz')
} else { } else {
log('QUIZ', 'Quiz has already been started, trying to finish it') this.bot.log('QUIZ', 'Quiz has already been started, trying to finish it')
} }
await wait(2000) await this.bot.utils.wait(2000)
let quizData = await getQuizData(page) let quizData = await this.bot.browser.func.getQuizData(page)
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
// All questions // All questions
@@ -31,7 +29,7 @@ export async function doQuiz(page: Page) {
const answers: string[] = [] const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') { if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
@@ -41,54 +39,53 @@ export async function doQuiz(page: Page) {
// Click the answers // Click the answers
for (const answer of 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 // Click the answer on page
await page.click(answer) await page.click(answer)
const refreshSuccess = await waitForQuizRefresh(page) const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) { if (!refreshSuccess) {
await page.close() await page.close()
log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return return
} }
} }
// Other type quiz // Other type quiz, lightspeed
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) { } else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
quizData = await getQuizData(page) // Refresh Quiz Data quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
const correctOption = quizData.correctAnswer const correctOption = quizData.correctAnswer
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
if (dataOption === correctOption) { if (dataOption === correctOption) {
// Click the answer on page // Click the answer on page
await page.click(`#rqAnswerOption${i}`) await page.click(`#rqAnswerOption${i}`)
const refreshSuccess = await waitForQuizRefresh(page) const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) { if (!refreshSuccess) {
await page.close() await page.close()
log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error') this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return return
} }
} }
} }
await wait(2000) await this.bot.utils.wait(2000)
} }
} }
// Done with // Done with
await wait(2000) await this.bot.utils.wait(2000)
await page.close() await page.close()
log('QUIZ', 'Completed the quiz successfully') this.bot.log('QUIZ', 'Completed the quiz successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
log('QUIZ', 'An error occurred:' + error, 'error') this.bot.log('QUIZ', 'An error occurred:' + error, 'error')
}
} }
} }

View File

@@ -1,67 +1,54 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import axios from 'axios' import axios from 'axios'
import { getLatestTab } from '../../browser/BrowserUtil' import { Workers } from '../Workers'
import { getSearchPoints } from '../../browser/BrowserFunc'
import { log } from '../../util/Logger'
import { randomNumber, shuffleArray, wait } from '../../util/Utils'
import { searchSettings } from '../../config.json' import { Counters, DashboardData } from '../../interface/DashboardData'
import { DashboardData, DashboardImpression } from '../../interface/DashboardData'
import { GoogleTrends } from '../../interface/GoogleDailyTrends' import { GoogleTrends } from '../../interface/GoogleDailyTrends'
import { GoogleSearch } from '../../interface/Search' import { GoogleSearch } from '../../interface/Search'
export async function doSearch(page: Page, data: DashboardData, mobile: boolean) {
log('SEARCH-BING', 'Starting bing searches')
const mobileData = data.userStatus.counters?.mobileSearch ? data.userStatus.counters.mobileSearch[0] : null // Mobile searches export class Search extends Workers {
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) ? private searchPageURL = 'https://bing.com'
(mobileData.pointProgressMax - mobileData.pointProgress) :
(edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress)
if (missingPoints == 0) { public async doSearch(page: Page, data: DashboardData) {
log('SEARCH-BING', `Bing searches for ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`) this.bot.log('SEARCH-BING', 'Starting bing searches')
page = await this.bot.browser.utils.getLatestTab(page)
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
let missingPoints = this.calculatePoints(searchCounters)
if (missingPoints === 0) {
this.bot.log('SEARCH-BING', `Bing searches for ${this.bot.isMobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
return return
} }
// Generate search queries // Generate search queries
let googleSearchQueries = await getGoogleTrends(data.userProfile.attributes.country, missingPoints) let googleSearchQueries = await this.getGoogleTrends(data.userProfile.attributes.country, missingPoints)
googleSearchQueries = shuffleArray(googleSearchQueries) googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Deduplicate the search terms // Deduplicate the search terms
googleSearchQueries = [...new Set(googleSearchQueries)] googleSearchQueries = [...new Set(googleSearchQueries)]
// Open a new tab
const browser = page.browser()
const searchPage = await browser.newPage()
// Go to bing // Go to bing
await searchPage.goto('https://bing.com') await page.goto(this.searchPageURL)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search
const queries: string[] = [] const queries: string[] = []
googleSearchQueries.forEach(x => queries.push(x.topic, ...x.related)) // Mobile search doesn't seem to like related queries?
googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) })
// Loop over Google search queries // Loop over Google search queries
for (let i = 0; i < queries.length; i++) { for (let i = 0; i < queries.length; i++) {
const query = queries[i] as string const query = queries[i] as string
log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`) this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${this.bot.isMobile}`)
const newData = await bingSearch(page, searchPage, query) searchCounters = await this.bingSearch(page, query)
const newMissingPoints = this.calculatePoints(searchCounters)
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
const newMissingPoints = (mobile && newMobileData) ?
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
// If the new point amount is the same as before // If the new point amount is the same as before
if (newMissingPoints == missingPoints) { if (newMissingPoints == missingPoints) {
@@ -72,41 +59,46 @@ export async function doSearch(page: Page, data: DashboardData, mobile: boolean)
missingPoints = newMissingPoints missingPoints = newMissingPoints
if (missingPoints == 0) { if (missingPoints === 0) {
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 break
} }
// If we didn't gain points for 10 iterations, assume it's stuck // If we didn't gain points for 10 iterations, assume it's stuck
if (maxLoop > 10) { if (maxLoop > 10) {
log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn') this.bot.log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
maxLoop = 0 // Reset to 0 so we can retry with related searches below maxLoop = 0 // Reset to 0 so we can retry with related searches below
break break
} }
} }
// Only for mobile searches
if (missingPoints > 0 && this.bot.isMobile) {
return
}
// If we still got remaining search queries, generate extra ones // If we still got remaining search queries, generate extra ones
if (missingPoints > 0) { if (missingPoints > 0) {
log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`) this.bot.log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
let i = 0 let i = 0
while (missingPoints > 0) { while (missingPoints > 0) {
const query = googleSearchQueries[i++] as GoogleSearch const query = googleSearchQueries[i++] as GoogleSearch
// Get related search terms to the Google search queries // Get related search terms to the Google search queries
const relatedTerms = await getRelatedTerms(query?.topic) const relatedTerms = await this.getRelatedTerms(query?.topic)
if (relatedTerms.length > 3) { if (relatedTerms.length > 3) {
// Search for the first 2 related terms // Search for the first 2 related terms
for (const term of relatedTerms.slice(1, 3)) { for (const term of relatedTerms.slice(1, 3)) {
log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${mobile}`) this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`)
const newData = await bingSearch(page, searchPage, query.topic)
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches searchCounters = await this.bingSearch(page, query.topic)
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches const newMissingPoints = this.calculatePoints(searchCounters)
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
const newMissingPoints = (mobile && newMobileData) ?
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
// If the new point amount is the same as before // If the new point amount is the same as before
if (newMissingPoints == missingPoints) { if (newMissingPoints == missingPoints) {
@@ -118,13 +110,13 @@ export async function doSearch(page: Page, data: DashboardData, mobile: boolean)
missingPoints = newMissingPoints missingPoints = newMissingPoints
// If we satisfied the searches // If we satisfied the searches
if (missingPoints == 0) { if (missingPoints === 0) {
break break
} }
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck // Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
if (maxLoop > 5) { if (maxLoop > 5) {
log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn') this.bot.log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
return return
} }
} }
@@ -132,17 +124,17 @@ export async function doSearch(page: Page, data: DashboardData, mobile: boolean)
} }
} }
log('SEARCH-BING', 'Completed searches') this.bot.log('SEARCH-BING', 'Completed searches')
} }
async function bingSearch(page: Page, searchPage: Page, query: string) { private async bingSearch(searchPage: Page, query: string) {
// Try a max of 5 times // Try a max of 5 times
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
try { try {
const searchBar = '#sb_form_q' 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 searchPage.click(searchBar) // Focus on the textarea
await wait(500) await this.bot.utils.wait(500)
await searchPage.keyboard.down('Control') await searchPage.keyboard.down('Control')
await searchPage.keyboard.press('A') await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace') await searchPage.keyboard.press('Backspace')
@@ -150,50 +142,54 @@ async function bingSearch(page: Page, searchPage: Page, query: string) {
await searchPage.keyboard.type(query) await searchPage.keyboard.type(query)
await searchPage.keyboard.press('Enter') await searchPage.keyboard.press('Enter')
if (searchSettings.scrollRandomResults) { if (this.bot.config.searchSettings.scrollRandomResults) {
await wait(2000) await this.bot.utils.wait(2000)
await randomScroll(searchPage) await this.randomScroll(searchPage)
} }
if (searchSettings.clickRandomResults) { if (this.bot.config.searchSettings.clickRandomResults) {
await wait(2000) await this.bot.utils.wait(2000)
await clickRandomLink(searchPage) await this.clickRandomLink(searchPage)
} }
await wait(Math.floor(randomNumber(10_000, 20_000))) await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
return await getSearchPoints(page) return await this.bot.browser.func.getSearchPoints()
} catch (error) { } catch (error) {
if (i === 5) { if (i === 5) {
log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error') this.bot.log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
break break
} }
log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error') this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn') this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
await wait(4000) // Reset the tabs
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
await this.closeTabs(lastTab, this.searchPageURL)
await this.bot.utils.wait(4000)
} }
} }
log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error') this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
return await getSearchPoints(page) return await this.bot.browser.func.getSearchPoints()
} }
async function getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> { private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> {
const queryTerms: GoogleSearch[] = [] const queryTerms: GoogleSearch[] = []
let i = 0 let i = 0
geoLocale = (searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US' geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US'
log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`) this.bot.log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
while (queryCount > queryTerms.length) { while (queryCount > queryTerms.length) {
i += 1 i += 1
const date = new Date() const date = new Date()
date.setDate(date.getDate() - i) date.setDate(date.getDate() - i)
const formattedDate = formatDate(date) const formattedDate = this.formatDate(date)
try { try {
const request = { const request = {
@@ -211,19 +207,19 @@ async function getGoogleTrends(geoLocale: string, queryCount: number): Promise<G
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) { for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
queryTerms.push({ queryTerms.push({
topic: topic.title.query.toLowerCase(), topic: topic.title.query.toLowerCase(),
related: topic.relatedQueries.map(x => x.query.toLocaleLowerCase()) related: topic.relatedQueries.map(x => x.query.toLowerCase())
}) })
} }
} catch (error) { } catch (error) {
log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error') this.bot.log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
} }
} }
return queryTerms return queryTerms
} }
async function getRelatedTerms(term: string): Promise<string[]> { private async getRelatedTerms(term: string): Promise<string[]> {
try { try {
const request = { const request = {
url: `https://api.bing.com/osjson.aspx?query=${term}`, url: `https://api.bing.com/osjson.aspx?query=${term}`,
@@ -237,12 +233,12 @@ async function getRelatedTerms(term: string): Promise<string[]> {
return response.data[1] as string[] return response.data[1] as string[]
} catch (error) { } catch (error) {
log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error') this.bot.log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error')
} }
return [] return []
} }
function formatDate(date: Date): string { private formatDate(date: Date): string {
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0')
@@ -250,82 +246,79 @@ function formatDate(date: Date): string {
return `${year}${month}${day}` return `${year}${month}${day}`
} }
async function randomScroll(page: Page) { private async randomScroll(page: Page) {
try { try {
// Press the arrow down key to scroll // Press the arrow down key to scroll
for (let i = 0; i < randomNumber(5, 100); i++) { for (let i = 0; i < this.bot.utils.randomNumber(5, 400); i++) {
await page.keyboard.press('ArrowDown') await page.keyboard.press('ArrowDown')
} }
} catch (error) { } catch (error) {
log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error') this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
} }
} }
async function clickRandomLink(page: Page) { private async clickRandomLink(page: Page) {
try { try {
const searchListingURL = new URL(page.url()) // Get searchPage info before clicking const searchListingURL = new URL(page.url()) // Get searchPage info before clicking
await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not
// Wait for website to load
await wait(3000)
// Will get current tab if no new one is created // Will get current tab if no new one is created
let lastTab = await getLatestTab(page) let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Wait for the body of the new page to be loaded // Let website load, if it doesn't load within 5 sec. exit regardless
await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { }) await this.bot.utils.wait(5000)
// Check if the tab is closed or not
if (!lastTab.isClosed()) {
let lastTabURL = new URL(lastTab.url()) // Get new tab info let lastTabURL = new URL(lastTab.url()) // Get new tab info
// Check if the URL is different from the original one, don't loop more than 5 times. // Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0 let i = 0
while (lastTabURL.href !== searchListingURL.href && i < 5) { while (lastTabURL.href !== searchListingURL.href && i < 5) {
// If hostname is still bing, (Bing images/news etc)
if (lastTabURL.hostname == searchListingURL.hostname) {
await lastTab.goBack()
lastTab = await getLatestTab(page) // Get last opened tab await this.closeTabs(lastTab, searchListingURL.href)
// If "goBack" didn't return to search listing (due to redirects) // End of loop, refresh lastPage
if (lastTabURL.hostname !== searchListingURL.hostname) { lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
await lastTab.goto(searchListingURL.href) lastTabURL = new URL(lastTab.url()) // Get new tab info
}
} else { // No longer on bing, likely opened a new tab, close this tab
lastTab = await getLatestTab(page) // Get last opened tab
lastTabURL = new URL(lastTab.url())
const tabs = await (page.browser()).pages() // Get all tabs open
// If the browser has more than 3 tabs open, it has opened a new one, we need to close this one.
if (tabs.length > 3) {
// Make sure the page is still open!
if (!lastTab.isClosed()) {
await lastTab.close()
}
} else if (lastTabURL.href !== searchListingURL.href) {
await lastTab.goBack()
lastTab = await getLatestTab(page) // Get last opened tab
lastTabURL = new URL(lastTab.url())
// If "goBack" didn't return to search listing (due to redirects)
if (lastTabURL.hostname !== searchListingURL.hostname) {
await lastTab.goto(searchListingURL.href)
}
}
}
}
lastTab = await getLatestTab(page) // Finally update the lastTab var again
i++ i++
} }
} catch (error) { } catch (error) {
log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error') this.bot.log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
}
}
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
const edgeData = counters.pcSearch?.[1] // Edge searches
const missingPoints = (this.bot.isMobile && mobileData)
? mobileData.pointProgressMax - mobileData.pointProgress
: (edgeData ? edgeData.pointProgressMax - edgeData.pointProgress : 0)
+ (genericData ? genericData.pointProgressMax - genericData.pointProgress : 0)
return missingPoints
} }
} }

View File

@@ -1,34 +1,47 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { wait } from '../../util/Utils' import { Workers } from '../Workers'
import { log } from '../../util/Logger'
import { getQuizData } from '../../browser/BrowserFunc'
export class ThisOrThat extends Workers {
async doThisOrThat(page: Page) {
this.bot.log('THIS-OR-THAT', 'Trying to complete ThisOrThat')
export async function doThisOrThat(page: Page) {
log('THIS-OR-THAT', 'Trying to complete ThisOrThat')
try { try {
await page.waitForNetworkIdle({ timeout: 5000 })
await wait(2000)
// Check if the quiz has been started or not // Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false) const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (quizNotStarted) { if (quizNotStarted) {
await page.click('#rqStartQuiz') await page.click('#rqStartQuiz')
} else { } else {
log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it') this.bot.log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
} }
await wait(2000) await this.bot.utils.wait(2000)
// Solving // Solving
const quizData = await getQuizData(page) const quizData = await this.bot.browser.func.getQuizData(page)
quizData // correctAnswer property is always null? const questionsRemaining = quizData.maxQuestions - (quizData.currentQuestionNumber - 1) // Amount of questions remaining
log('THIS-OR-THAT', 'Completed the ThisOrthat successfully') for (let question = 0; question < questionsRemaining; question++) {
// Since there's no solving logic yet, randomly guess to complete
const buttonId = `#rqAnswerOption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.click(buttonId)
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
await page.close()
this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
}
this.bot.log('THIS-OR-THAT', 'Completed the ThisOrThat successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
log('THIS-OR-THAT', 'An error occurred:' + error, 'error') this.bot.log('THIS-OR-THAT', 'An error occurred:' + error, 'error')
}
} }
} }

View File

@@ -1,21 +1,23 @@
import { Page } from 'puppeteer' import { Page } from 'playwright'
import { wait } from '../../util/Utils' import { Workers } from '../Workers'
import { log } from '../../util/Logger'
export async function doUrlReward(page: Page) {
log('URL-REWARD', 'Trying to complete UrlReward') export class UrlReward extends Workers {
async doUrlReward(page: Page) {
this.bot.log('URL-REWARD', 'Trying to complete UrlReward')
try { try {
// After waiting, close the page this.bot.utils.wait(2000)
await page.waitForNetworkIdle({ timeout: 10_000 })
await wait(2000)
await page.close() await page.close()
log('URL-REWARD', 'Completed the UrlReward successfully') this.bot.log('URL-REWARD', 'Completed the UrlReward successfully')
} catch (error) { } catch (error) {
await page.close() await page.close()
log('URL-REWARD', 'An error occurred:' + error, 'error') this.bot.log('URL-REWARD', 'An error occurred:' + error, 'error')
}
} }
} }

View File

@@ -1,40 +1,63 @@
import cluster from 'cluster' import cluster from 'cluster'
import { BrowserContext, Page } from 'playwright'
import Browser from './browser/Browser' import Browser from './browser/Browser'
import { getDashboardData, getEarnablePoints, goHome } from './browser/BrowserFunc' import BrowserFunc from './browser/BrowserFunc'
import BrowserUtil from './browser/BrowserUtil'
import { log } from './util/Logger' import { log } from './util/Logger'
import { loadAccounts } from './util/Account' import Util from './util/Utils'
import { chunkArray } from './util/Utils' import { loadAccounts, loadConfig, saveSessionData } from './util/Load'
import { login } from './functions/Login' import { Login } from './functions/Login'
import { doDailySet, doMorePromotions, doPunchCard } from './functions/Workers' import { Workers } from './functions/Workers'
import { doSearch } from './functions/activities/Search' import Activities from './functions/Activities'
import { Account } from './interface/Account' import { Account } from './interface/Account'
import { runOnZeroPoints, workers, clusters } from './config.json'
// Main bot class // Main bot class
class MicrosoftRewardsBot { export class MicrosoftRewardsBot {
private activeWorkers: number = clusters public log: typeof log
public config
public utils: Util
public activities: Activities = new Activities(this)
public browser: {
func: BrowserFunc,
utils: BrowserUtil
}
public isMobile: boolean = false
public homePage!: Page
private collectedPoints: number = 0 private collectedPoints: number = 0
private browserFactory: Browser = new Browser() private activeWorkers: number
private browserFactory: Browser = new Browser(this)
private accounts: Account[] private accounts: Account[]
private workers: Workers
private login = new Login(this)
constructor() { constructor() {
this.log = log
this.accounts = [] this.accounts = []
this.utils = new Util()
this.workers = new Workers(this)
this.browser = {
func: new BrowserFunc(this),
utils: new BrowserUtil(this)
}
this.config = loadConfig()
this.activeWorkers = this.config.clusters
} }
async initialize() { async initialize() {
this.accounts = await loadAccounts() this.accounts = loadAccounts()
} }
async run() { async run() {
log('MAIN', `Bot started with ${clusters} clusters`) log('MAIN', `Bot started with ${this.config.clusters} clusters`)
// Only cluster when there's more than 1 cluster demanded // Only cluster when there's more than 1 cluster demanded
if (clusters > 1) { if (this.config.clusters > 1) {
if (cluster.isPrimary) { if (cluster.isPrimary) {
this.runMaster() this.runMaster()
} else { } else {
@@ -48,7 +71,7 @@ class MicrosoftRewardsBot {
private runMaster() { private runMaster() {
log('MAIN-PRIMARY', 'Primary process started') log('MAIN-PRIMARY', 'Primary process started')
const accountChunks = chunkArray(this.accounts, clusters) const accountChunks = this.utils.chunkArray(this.accounts, this.config.clusters)
for (let i = 0; i < accountChunks.length; i++) { for (let i = 0; i < accountChunks.length; i++) {
const worker = cluster.fork() const worker = cluster.fork()
@@ -71,7 +94,6 @@ class MicrosoftRewardsBot {
private runWorker() { private runWorker() {
log('MAIN-WORKER', `Worker ${process.pid} spawned`) log('MAIN-WORKER', `Worker ${process.pid} spawned`)
// Receive the chunk of accounts from the master // Receive the chunk of accounts from the master
process.on('message', async ({ chunk }) => { process.on('message', async ({ chunk }) => {
await this.runTasks(chunk) await this.runTasks(chunk)
@@ -86,7 +108,7 @@ class MicrosoftRewardsBot {
await this.Desktop(account) await this.Desktop(account)
// If runOnZeroPoints is false and 0 points to earn, stop and try the next account // If runOnZeroPoints is false and 0 points to earn, stop and try the next account
if (!runOnZeroPoints && this.collectedPoints === 0) { if (!this.config.runOnZeroPoints && this.collectedPoints === 0) {
continue continue
} }
@@ -103,116 +125,136 @@ class MicrosoftRewardsBot {
// Desktop // Desktop
async Desktop(account: Account) { async Desktop(account: Account) {
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, false) const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
const page = await browser.newPage() 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 page.authenticate({ username: account.proxy.username, password: account.proxy.password })
log('MAIN', 'Starting DESKTOP browser') log('MAIN', 'Starting DESKTOP browser')
// Login into MS Rewards // Login into MS Rewards, then go to rewards homepage
await login(page, account.email, account.password) await this.login.login(this.homePage, account.email, account.password)
await this.browser.func.goHome(this.homePage)
const wentHome = await goHome(page) const data = await this.browser.func.getDashboardData()
if (!wentHome) {
throw log('MAIN', 'Unable to get dashboard page', 'error')
}
const data = await getDashboardData(page)
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`) log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
const earnablePoints = await getEarnablePoints(data) const earnablePoints = await this.browser.func.getEarnablePoints()
this.collectedPoints = earnablePoints this.collectedPoints = earnablePoints
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`) log('MAIN-POINTS', `You can earn ${earnablePoints} points today`)
// If runOnZeroPoints is false and 0 points to earn, don't continue // If runOnZeroPoints is false and 0 points to earn, don't continue
if (!runOnZeroPoints && this.collectedPoints === 0) { if (!this.config.runOnZeroPoints && this.collectedPoints === 0) {
log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping') log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!')
// Close desktop browser // 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
const workerPage = await browser.newPage()
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// Complete daily set // Complete daily set
if (workers.doDailySet) { if (this.config.workers.doDailySet) {
await doDailySet(page, data) await this.workers.doDailySet(workerPage, data)
} }
// Complete more promotions // Complete more promotions
if (workers.doMorePromotions) { if (this.config.workers.doMorePromotions) {
await doMorePromotions(page, data) await this.workers.doMorePromotions(workerPage, data)
} }
// Complete punch cards // Complete punch cards
if (workers.doPunchCards) { if (this.config.workers.doPunchCards) {
await doPunchCard(page, data) await this.workers.doPunchCard(workerPage, data)
} }
// Do desktop searches // Do desktop searches
if (workers.doDesktopSearch) { if (this.config.workers.doDesktopSearch) {
await doSearch(page, data, false) 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 // Close desktop browser
await browser.close() return await this.closeBrowser(browser, account.email)
} }
// Mobile // Mobile
async Mobile(account: Account) { async Mobile(account: Account) {
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, true) this.isMobile = true
const page = await browser.newPage()
let pages = await browser.pages()
// If for some reason the browser initializes with more than 2 pages, close these const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
while (pages.length > 2) { this.homePage = await browser.newPage()
await pages[0]?.close()
pages = await browser.pages()
}
// Log into proxy
await page.authenticate({ username: account.proxy.username, password: account.proxy.password })
log('MAIN', 'Starting MOBILE browser') log('MAIN', 'Starting MOBILE browser')
// Login into MS Rewards // Login into MS Rewards, then go to rewards homepage
await login(page, account.email, account.password) await this.login.login(this.homePage, account.email, account.password)
await this.browser.func.goHome(this.homePage)
await goHome(page) const data = await this.browser.func.getDashboardData()
const data = await getDashboardData(page)
// If no mobile searches data found, stop (Does not exist on new accounts) // If no mobile searches data found, stop (Does not exist on new accounts)
if (!data.userStatus.counters.mobileSearch) { if (!data.userStatus.counters.mobileSearch) {
log('MAIN', 'No mobile searches found, stopping') log('MAIN', 'No mobile searches found, stopping!')
// Close mobile browser // 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
const workerPage = await browser.newPage()
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// Do mobile searches // Do mobile searches
if (workers.doMobileSearch) { if (this.config.workers.doMobileSearch) {
await doSearch(page, data, true) await this.activities.doSearch(workerPage, data)
// Fetch current search points
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
// 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 this.closeBrowser(browser, account.email)
// Retry
await this.Mobile(account)
}
} }
// Fetch new points // Fetch new points
const earnablePoints = await getEarnablePoints(data, page) const earnablePoints = await this.browser.func.getEarnablePoints()
// If the new earnable is 0, means we got all the points, else retract // If the new earnable is 0, means we got all the points, else retract
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints) this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`) log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
// Close mobile browser // 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() await browser.close()
} }
} }
const bot = new MicrosoftRewardsBot() const bot = new MicrosoftRewardsBot()
// Initialize accounts first and then start the bot // Initialize accounts first and then start the bot

37
src/interface/Config.ts Normal file
View File

@@ -0,0 +1,37 @@
export interface Config {
baseURL: string;
sessionPath: string;
headless: boolean;
runOnZeroPoints: boolean;
clusters: number;
workers: Workers;
searchSettings: SearchSettings;
webhook: Webhook;
saveFingerprint: boolean;
}
export interface SearchSettings {
useGeoLocaleQueries: boolean;
scrollRandomResults: boolean;
clickRandomResults: boolean;
searchDelay: SearchDelay;
retryMobileSearch: boolean;
}
export interface SearchDelay {
min: number;
max: number;
}
export interface Webhook {
enabled: boolean;
url: string;
}
export interface Workers {
doDailySet: boolean;
doMorePromotions: boolean;
doPunchCards: boolean;
doDesktopSearch: boolean;
doMobileSearch: boolean;
}

View File

@@ -1,22 +0,0 @@
import * as fs from 'fs'
import path from 'path'
import { Account } from '../interface/Account'
export async function loadAccounts(): Promise<Account[]> {
try {
let file = 'accounts.json'
// If dev mode, use dev account(s)
if (process.argv.includes('-dev')) {
file = 'accounts.dev.json'
}
const accountDir = path.join(__dirname, '../', file)
const accounts = fs.readFileSync(accountDir, 'utf-8')
return JSON.parse(accounts)
} catch (error) {
throw new Error(error as string)
}
}

106
src/util/Load.ts Normal file
View File

@@ -0,0 +1,106 @@
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'
export function loadAccounts(): Account[] {
try {
let file = 'accounts.json'
// If dev mode, use dev account(s)
if (process.argv.includes('-dev')) {
file = 'accounts.dev.json'
}
const accountDir = path.join(__dirname, '../', file)
const accounts = fs.readFileSync(accountDir, 'utf-8')
return JSON.parse(accounts)
} catch (error) {
throw new Error(error as string)
}
}
export function loadConfig(): Config {
try {
const configDir = path.join(__dirname, '../', 'config.json')
const config = fs.readFileSync(configDir, 'utf-8')
return JSON.parse(config)
} catch (error) {
throw new Error(error as string)
}
}
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<string> {
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 cookies to a file
await fs.promises.writeFile(path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`), JSON.stringify(cookies))
return sessionDir
} catch (error) {
throw new Error(error as string)
}
}
export async function saveFingerprintData(sessionPath: string, email: string, isMobile: boolean, fingerpint: BrowserFingerprintWithHeaders): Promise<string> {
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)
}
}

View File

@@ -1,7 +1,7 @@
import { Webhook } from './Webhook' import { Webhook } from './Webhook'
export function log(title: string, message: string, type?: 'log' | 'warn' | 'error') { export function log(title: string, message: string, type?: 'log' | 'warn' | 'error') {
const currentTime = new Date().toISOString() const currentTime = new Date().toLocaleString()
let str = '' let str = ''

View File

@@ -81,7 +81,7 @@ export async function getEdgeVersions() {
export function getSystemComponents(mobile: boolean): string { export function getSystemComponents(mobile: boolean): string {
const osId: string = mobile ? 'Linux' : 'Windows NT 10.0' 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) { if (mobile) {
return `${uaPlatform}; ${osId}; K` return `${uaPlatform}; ${osId}; K`

View File

@@ -1,10 +1,12 @@
export async function wait(ms: number): Promise<void> { export default class Util {
async wait(ms: number): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
setTimeout(resolve, ms) setTimeout(resolve, ms)
}) })
} }
export function getFormattedDate(ms = Date.now()): string { getFormattedDate(ms = Date.now()): string {
const today = new Date(ms) const today = new Date(ms)
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0 const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0
const day = String(today.getDate()).padStart(2, '0') const day = String(today.getDate()).padStart(2, '0')
@@ -13,7 +15,7 @@ export function getFormattedDate(ms = Date.now()): string {
return `${month}/${day}/${year}` return `${month}/${day}/${year}`
} }
export function shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
const shuffledArray = array.slice() const shuffledArray = array.slice()
shuffledArray.sort(() => Math.random() - 0.5) shuffledArray.sort(() => Math.random() - 0.5)
@@ -21,11 +23,11 @@ export function shuffleArray<T>(array: T[]): T[] {
return shuffledArray return shuffledArray
} }
export function randomNumber(min: number, max: number): number { randomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min return Math.floor(Math.random() * (max - min + 1)) + min
} }
export function chunkArray<T>(arr: T[], numChunks: number): T[][] { chunkArray<T>(arr: T[], numChunks: number): T[][] {
const chunkSize = Math.ceil(arr.length / numChunks) const chunkSize = Math.ceil(arr.length / numChunks)
const chunks: T[][] = [] const chunks: T[][] = []
@@ -36,3 +38,5 @@ export function chunkArray<T>(arr: T[], numChunks: number): T[][] {
return chunks return chunks
} }
}

View File

@@ -1,8 +1,11 @@
import axios from 'axios' import axios from 'axios'
import { webhook } from '../config.json' import { loadConfig } from './Load'
export async function Webhook(content: string) { export async function Webhook(content: string) {
const webhook = loadConfig().webhook
if (!webhook.enabled || webhook.url.length < 10) return if (!webhook.enabled || webhook.url.length < 10) return
const request = { const request = {

View File

@@ -64,7 +64,8 @@
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/accounts.json" "src/accounts.json",
"src/config.json"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"