mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-12 03:16:19 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea7f5c452 | ||
|
|
f3fb641ecd | ||
|
|
bca1e7c896 | ||
|
|
fd7c8e36d4 | ||
|
|
dcb0c25d46 | ||
|
|
a8cb5482d4 | ||
|
|
28286ff9fe | ||
|
|
1d6167aeca | ||
|
|
3b15fe19a7 |
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
10
README.md
10
README.md
@@ -1,5 +1,5 @@
|
||||
# Microsoft-Rewards-Script
|
||||
Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Puppeteer.
|
||||
Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Playwright.
|
||||
|
||||
Under development, however mainly for personal use!
|
||||
|
||||
@@ -11,6 +11,11 @@ Under development, however mainly for personal use!
|
||||
5. Run `npm run build` to build the 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 ##
|
||||
- [x] Multi-Account Support
|
||||
- [x] Session Storing
|
||||
@@ -31,8 +36,7 @@ Under development, however mainly for personal use!
|
||||
- [x] Completing Click Rewards
|
||||
- [x] Completing Polls
|
||||
- [x] Completing Punchcards
|
||||
- [ ] Solving This Or That Quiz
|
||||
- [x] Clicking Promotional Items
|
||||
- [x] Solving This Or That Quiz (Random)
|
||||
- [x] Solving ABC Quiz
|
||||
- [ ] Completing Shopping Game
|
||||
- [ ] Completing Gaming Tab
|
||||
|
||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "1.2.4",
|
||||
"version": "1.4.0",
|
||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
@@ -10,7 +10,8 @@
|
||||
"build": "tsc",
|
||||
"start": "node ./dist/index.js",
|
||||
"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": [
|
||||
"Bing Rewards",
|
||||
@@ -18,24 +19,23 @@
|
||||
"Bot",
|
||||
"Script",
|
||||
"TypeScript",
|
||||
"Puppeteer",
|
||||
"Playwright",
|
||||
"Cheerio"
|
||||
],
|
||||
"author": "Netsky",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@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": {
|
||||
"axios": "^1.5.1",
|
||||
"axios": "^1.6.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-modules-newline": "^0.0.6",
|
||||
"fingerprint-generator": "^2.1.42",
|
||||
"fingerprint-injector": "^2.1.42",
|
||||
"puppeteer": "^21.4.1",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"ts-node": "^10.9.1"
|
||||
"fingerprint-generator": "^2.1.46",
|
||||
"fingerprint-injector": "^2.1.46",
|
||||
"playwright": "^1.40.1",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import puppeteer from 'puppeteer-extra'
|
||||
import { FingerprintInjector } from 'fingerprint-injector'
|
||||
import playwright from 'playwright'
|
||||
import { BrowserContext } from 'playwright'
|
||||
|
||||
import { newInjectedContext } from 'fingerprint-injector'
|
||||
import { FingerprintGenerator } from 'fingerprint-generator'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
|
||||
@@ -12,7 +15,6 @@ https://botcheck.luminati.io/
|
||||
http://f.vision/
|
||||
*/
|
||||
|
||||
|
||||
class Browser {
|
||||
private bot: MicrosoftRewardsBot
|
||||
|
||||
@@ -20,38 +22,47 @@ class Browser {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async createBrowser(email: string, proxy: AccountProxy, isMobile: boolean) {
|
||||
// const userAgent = await getUserAgent(isMobile)
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
|
||||
const browser = await playwright.chromium.launch({
|
||||
//channel: 'msedge', // Uses Edge instead of chrome
|
||||
headless: this.bot.config.headless,
|
||||
userDataDir: await this.bot.browser.func.loadSesion(email),
|
||||
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--mute-audio',
|
||||
'--disable-setuid-sandbox',
|
||||
proxy.url ? `--proxy-server=${proxy.url}:${proxy.port}` : ''
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors'
|
||||
]
|
||||
})
|
||||
|
||||
const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
|
||||
devices: isMobile ? ['mobile'] : ['desktop'],
|
||||
operatingSystems: isMobile ? ['android'] : ['windows'],
|
||||
browsers: ['edge'],
|
||||
browserListQuery: 'last 2 Edge versions'
|
||||
})
|
||||
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
|
||||
|
||||
// Modify the newPage function to attach the fingerprint
|
||||
const originalNewPage = browser.newPage
|
||||
browser.newPage = async function () {
|
||||
const page = await originalNewPage.apply(browser)
|
||||
await new FingerprintInjector().attachFingerprintToPuppeteer(page, { fingerprint, headers })
|
||||
return page
|
||||
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)
|
||||
}
|
||||
|
||||
return browser
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export default Browser
|
||||
export default Browser
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
import { CheerioAPI, load } from 'cheerio'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
@@ -16,11 +14,20 @@ export default class BrowserFunc {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async goHome(page: Page): Promise<boolean> {
|
||||
|
||||
/**
|
||||
* Navigate the provided page to rewards homepage
|
||||
* @param {Page} page Playwright page
|
||||
*/
|
||||
async goHome(page: Page) {
|
||||
|
||||
try {
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
|
||||
if (page.url() === dashboardURL.href) {
|
||||
return
|
||||
}
|
||||
|
||||
await page.goto(this.bot.config.baseURL)
|
||||
|
||||
const maxIterations = 5 // Maximum iterations set to 5
|
||||
@@ -30,7 +37,7 @@ export default class BrowserFunc {
|
||||
await this.bot.browser.utils.tryDismissCookieBanner(page)
|
||||
|
||||
// Check if account is suspended
|
||||
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
|
||||
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (isSuspended) {
|
||||
this.bot.log('GO-HOME', 'This account is suspended!', 'error')
|
||||
throw new Error('Account has been suspended!')
|
||||
@@ -39,12 +46,14 @@ export default class BrowserFunc {
|
||||
try {
|
||||
// If activities are found, exit the loop
|
||||
await page.waitForSelector('#more-activities', { timeout: 1000 })
|
||||
this.bot.log('GO-HOME', 'Visited homepage successfully')
|
||||
break
|
||||
|
||||
} catch (error) {
|
||||
// Continue if element is not found
|
||||
}
|
||||
|
||||
// Below runs if the homepage was unable to be visited
|
||||
const currentURL = new URL(page.url())
|
||||
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
@@ -52,34 +61,37 @@ export default class BrowserFunc {
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
await page.goto(this.bot.config.baseURL)
|
||||
} else {
|
||||
this.bot.log('GO-HOME', 'Visited homepage successfully')
|
||||
break
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(5000)
|
||||
this.bot.log('GO-HOME', 'Visited homepage successfully')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('An error occurred:', error)
|
||||
return false
|
||||
throw this.bot.log('GO-HOME', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async getDashboardData(page: Page): Promise<DashboardData> {
|
||||
/**
|
||||
* Fetch user dashboard data
|
||||
* @returns {DashboardData} Object of user bing rewards dashboard data
|
||||
*/
|
||||
async getDashboardData(): Promise<DashboardData> {
|
||||
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||
const currentURL = new URL(page.url())
|
||||
const currentURL = new URL(this.bot.homePage.url())
|
||||
|
||||
// Should never happen since tasks are opened in a new tab!
|
||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||
this.bot.log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||
await this.goHome(page)
|
||||
await this.goHome(this.bot.homePage)
|
||||
}
|
||||
|
||||
// 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 targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
|
||||
@@ -91,7 +103,7 @@ export default class BrowserFunc {
|
||||
})
|
||||
|
||||
// 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
|
||||
const regex = /var dashboard = (\{.*?\});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
@@ -106,47 +118,23 @@ export default class BrowserFunc {
|
||||
return dashboardData
|
||||
}
|
||||
|
||||
async getQuizData(page: Page): Promise<QuizData> {
|
||||
try {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const scriptContent = $('script').filter((index, element) => {
|
||||
return $(element).text().includes('_w.rewardsQuizRenderInfo')
|
||||
}).text()
|
||||
|
||||
if (scriptContent) {
|
||||
const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
|
||||
if (match && match[1]) {
|
||||
const quizData = JSON.parse(match[1])
|
||||
return quizData
|
||||
} else {
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error')
|
||||
}
|
||||
} else {
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getSearchPoints(page: Page): Promise<Counters> {
|
||||
const dashboardData = await this.getDashboardData(page) // Always fetch newest data
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
async getEarnablePoints(data: DashboardData, page: null | Page = null): Promise<number> {
|
||||
/**
|
||||
* Get total earnable points
|
||||
* @returns {number} Total earnable points
|
||||
*/
|
||||
async getEarnablePoints(): Promise<number> {
|
||||
try {
|
||||
// Fetch new data if page is provided
|
||||
if (page) {
|
||||
data = await this.getDashboardData(page)
|
||||
}
|
||||
const data = await this.getDashboardData()
|
||||
|
||||
// These only include the points from tasks that the script can complete!
|
||||
let totalEarnablePoints = 0
|
||||
@@ -176,12 +164,13 @@ export default class BrowserFunc {
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentPoints(data: DashboardData, page: null | Page = null): Promise<number> {
|
||||
/**
|
||||
* Get current point amount
|
||||
* @returns {number} Current total point amount
|
||||
*/
|
||||
async getCurrentPoints(): Promise<number> {
|
||||
try {
|
||||
// Fetch new data if page is provided
|
||||
if (page) {
|
||||
data = await this.getDashboardData(page)
|
||||
}
|
||||
const data = await this.getDashboardData()
|
||||
|
||||
return data.userStatus.availablePoints
|
||||
} catch (error) {
|
||||
@@ -189,24 +178,43 @@ export default class BrowserFunc {
|
||||
}
|
||||
}
|
||||
|
||||
async loadSesion(email: string): Promise<string> {
|
||||
const sessionDir = path.join(__dirname, this.bot.config.sessionPath, email)
|
||||
|
||||
/**
|
||||
* Parse quiz data from provided page
|
||||
* @param {Page} page Playwright page
|
||||
* @returns {QuizData} Quiz data object
|
||||
*/
|
||||
async getQuizData(page: Page): Promise<QuizData> {
|
||||
try {
|
||||
// Create session dir
|
||||
if (!fs.existsSync(sessionDir)) {
|
||||
await fs.promises.mkdir(sessionDir, { recursive: true })
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const scriptContent = $('script').filter((index, element) => {
|
||||
return $(element).text().includes('_w.rewardsQuizRenderInfo')
|
||||
}).text()
|
||||
|
||||
if (scriptContent) {
|
||||
const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s
|
||||
const match = regex.exec(scriptContent)
|
||||
|
||||
if (match && match[1]) {
|
||||
const quizData = JSON.parse(match[1])
|
||||
return quizData
|
||||
} else {
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error')
|
||||
}
|
||||
} else {
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||
}
|
||||
|
||||
return sessionDir
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
throw this.bot.log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async waitForQuizRefresh(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('#rqHeaderCredits', { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
return true
|
||||
@@ -218,7 +226,7 @@ export default class BrowserFunc {
|
||||
|
||||
async checkQuizCompleted(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 2000 })
|
||||
await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
return true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class BrowserUtil {
|
||||
|
||||
for (const button of buttons) {
|
||||
try {
|
||||
const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 })
|
||||
const element = await page.waitForSelector(button.selector, { state: 'visible', timeout: 1000 })
|
||||
if (element) {
|
||||
await element.click()
|
||||
result = true
|
||||
@@ -73,8 +73,8 @@ export default class BrowserUtil {
|
||||
try {
|
||||
await this.bot.utils.wait(500)
|
||||
|
||||
const browser = page.browser()
|
||||
const pages = await browser.pages()
|
||||
const browser = page.context()
|
||||
const pages = browser.pages()
|
||||
const newTab = pages[pages.length - 1]
|
||||
|
||||
if (newTab) {
|
||||
@@ -87,4 +87,38 @@ export default class BrowserUtil {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
"sessionPath": "sessions",
|
||||
"headless": false,
|
||||
"runOnZeroPoints": false,
|
||||
"runOnZeroPoints": true,
|
||||
"clusters": 1,
|
||||
"saveFingerprint": false,
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
@@ -18,7 +19,8 @@
|
||||
"searchDelay": {
|
||||
"min": 10000,
|
||||
"max": 20000
|
||||
}
|
||||
},
|
||||
"retryMobileSearch": true
|
||||
},
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
@@ -19,9 +19,9 @@ export default class Activities {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
doSearch = async (page: Page, data: DashboardData, mobile: boolean): Promise<void> => {
|
||||
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
|
||||
const search = new Search(this.bot)
|
||||
await search.doSearch(page, data, mobile)
|
||||
await search.doSearch(page, data)
|
||||
}
|
||||
|
||||
doABC = async (page: Page): Promise<void> => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
import readline from 'readline'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
@@ -20,18 +20,19 @@ export class Login {
|
||||
|
||||
try {
|
||||
// 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: 10_000 }).then(() => true).catch(() => false)
|
||||
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
|
||||
|
||||
if (!isLoggedIn) {
|
||||
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 10_000 }).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) {
|
||||
this.bot.log('LOGIN', 'This account has been locked!', 'error')
|
||||
throw new Error('Account has been locked!')
|
||||
}
|
||||
|
||||
await page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector('#loginHeader', { state: 'visible', timeout: 10_000 })
|
||||
|
||||
await this.execLogin(page, email, password)
|
||||
this.bot.log('LOGIN', 'Logged into Microsoft successfully')
|
||||
@@ -58,15 +59,17 @@ export class Login {
|
||||
this.bot.log('LOGIN', 'Email entered successfully')
|
||||
|
||||
try {
|
||||
await page.waitForSelector('#i0118', { visible: true, timeout: 2000 })
|
||||
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
await page.type('#i0118', password)
|
||||
await page.click('#idSIButton9')
|
||||
|
||||
// When erroring at this stage it means a 2FA code is required
|
||||
} catch (error) {
|
||||
this.bot.log('LOGIN', '2FA code required')
|
||||
|
||||
// Wait for user input
|
||||
const code = await new Promise<string>((resolve) => {
|
||||
rl.question('Enter 2FA code:\n', (input) => {
|
||||
rl.close()
|
||||
@@ -83,13 +86,13 @@ export class Login {
|
||||
|
||||
const currentURL = new URL(page.url())
|
||||
|
||||
while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') {
|
||||
while (currentURL.pathname !== '/' || currentURL.hostname !== 'rewards.bing.com') {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
currentURL.href = page.url()
|
||||
}
|
||||
|
||||
// Wait for login to complete
|
||||
await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 })
|
||||
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 })
|
||||
}
|
||||
|
||||
private async checkBingLogin(page: Page): Promise<void> {
|
||||
@@ -103,11 +106,11 @@ export class Login {
|
||||
const currentUrl = new URL(page.url())
|
||||
|
||||
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
|
||||
await this.bot.utils.wait(3000)
|
||||
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
|
||||
|
||||
const loggedIn = await this.checkBingLoginStatus(page)
|
||||
if (loggedIn) {
|
||||
// If mobile browser, skip this step
|
||||
if (loggedIn || this.bot.isMobile) {
|
||||
this.bot.log('LOGIN-BING', 'Bing login verification passed!')
|
||||
break
|
||||
}
|
||||
@@ -123,7 +126,7 @@ export class Login {
|
||||
|
||||
private async checkBingLoginStatus(page: Page): Promise<boolean> {
|
||||
try {
|
||||
await page.waitForSelector('#id_n', { timeout: 10_000 })
|
||||
await page.waitForSelector('#id_n', { timeout: 5000 })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
|
||||
|
||||
@@ -27,6 +27,11 @@ export class Workers {
|
||||
|
||||
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('DAILY-SET', 'All "Daily Set" items have been completed')
|
||||
}
|
||||
|
||||
@@ -41,21 +46,31 @@ export class Workers {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Solve Activities
|
||||
this.bot.log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
|
||||
|
||||
const browser = page.browser()
|
||||
page = await browser.newPage()
|
||||
|
||||
// Got to punch card index page in a new tab
|
||||
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
|
||||
|
||||
// Wait for new page to load, max 10 seconds, however try regardless in case of error
|
||||
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { })
|
||||
|
||||
await this.solveActivities(page, activitiesUncompleted, punchCard)
|
||||
|
||||
// Close the punch card index page
|
||||
await page.close()
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
const pages = page.context().pages()
|
||||
|
||||
if (pages.length > 3) {
|
||||
await page.close()
|
||||
} else {
|
||||
await this.bot.browser.func.goHome(page)
|
||||
}
|
||||
|
||||
this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
|
||||
}
|
||||
@@ -82,45 +97,63 @@ export class Workers {
|
||||
// Solve Activities
|
||||
this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" item')
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
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
|
||||
private async 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) {
|
||||
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}"]`
|
||||
|
||||
if (punchCard) {
|
||||
selector = await this.bot.browser.func.getPunchCardActivity(page, activity)
|
||||
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
|
||||
|
||||
} 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: 10_000 })
|
||||
|
||||
// Click element, it will be opened in a new tab
|
||||
await page.click(selector)
|
||||
|
||||
// Cooldown
|
||||
await this.bot.utils.wait(4000)
|
||||
await activityPage.click(selector)
|
||||
|
||||
// Select the new activity page
|
||||
const activityPage = await this.bot.browser.utils.getLatestTab(page)
|
||||
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||
|
||||
// Wait for body to load
|
||||
await activityPage.waitForSelector('body', { timeout: 10_000 })
|
||||
// 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) {
|
||||
// Quiz (Poll, Quiz or ABC)
|
||||
@@ -167,9 +200,11 @@ export class Workers {
|
||||
|
||||
// Cooldown
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
@@ -15,18 +15,18 @@ export class ABC extends Workers {
|
||||
const maxIterations = 15
|
||||
let i
|
||||
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
|
||||
await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 })
|
||||
|
||||
const answers = $('.wk_OptionClickClass')
|
||||
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
|
||||
|
||||
await page.waitForSelector(`#${answer}`, { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 })
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
await page.click(`#${answer}`) // Click answer
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
await page.waitForSelector('div.wk_button', { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 })
|
||||
await page.click('div.wk_button') // Click next question button
|
||||
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
@@ -11,7 +11,7 @@ export class Poll extends Workers {
|
||||
try {
|
||||
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
|
||||
|
||||
await page.waitForSelector(buttonId, { visible: true, timeout: 10_000 })
|
||||
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not
|
||||
await this.bot.utils.wait(2000)
|
||||
|
||||
await page.click(buttonId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
@@ -10,7 +10,7 @@ export class Quiz extends Workers {
|
||||
|
||||
try {
|
||||
// Check if the quiz has been started or not
|
||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
|
||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||
if (quizNotStarted) {
|
||||
await page.click('#rqStartQuiz')
|
||||
} else {
|
||||
@@ -29,7 +29,7 @@ export class Quiz extends Workers {
|
||||
const answers: string[] = []
|
||||
|
||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 })
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
|
||||
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
|
||||
|
||||
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
|
||||
@@ -39,7 +39,7 @@ export class Quiz extends Workers {
|
||||
|
||||
// Click the answers
|
||||
for (const answer of answers) {
|
||||
await page.waitForSelector(answer, { visible: true, timeout: 2000 })
|
||||
await page.waitForSelector(answer, { state: 'visible', timeout: 2000 })
|
||||
|
||||
// Click the answer on page
|
||||
await page.click(answer)
|
||||
@@ -52,14 +52,14 @@ export class Quiz extends Workers {
|
||||
}
|
||||
}
|
||||
|
||||
// Other type quiz
|
||||
// Other type quiz, lightspeed
|
||||
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
|
||||
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
|
||||
const correctOption = quizData.correctAnswer
|
||||
|
||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
||||
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 })
|
||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
|
||||
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
|
||||
|
||||
if (dataOption === correctOption) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
import axios from 'axios'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
import { DashboardData, DashboardImpression } from '../../interface/DashboardData'
|
||||
import { Counters, DashboardData } from '../../interface/DashboardData'
|
||||
import { GoogleTrends } from '../../interface/GoogleDailyTrends'
|
||||
import { GoogleSearch } from '../../interface/Search'
|
||||
|
||||
@@ -12,19 +12,16 @@ export class Search extends Workers {
|
||||
|
||||
private searchPageURL = 'https://bing.com'
|
||||
|
||||
public async doSearch(page: Page, data: DashboardData, mobile: boolean) {
|
||||
public async doSearch(page: Page, data: DashboardData) {
|
||||
this.bot.log('SEARCH-BING', 'Starting bing searches')
|
||||
|
||||
const mobileData = data.userStatus.counters?.mobileSearch ? data.userStatus.counters.mobileSearch[0] : null // Mobile searches
|
||||
const edgeData = data.userStatus.counters.pcSearch[1] as DashboardImpression // Edge searches
|
||||
const genericData = data.userStatus.counters.pcSearch[0] as DashboardImpression // Normal searches
|
||||
page = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
let missingPoints = (mobile && mobileData) ?
|
||||
(mobileData.pointProgressMax - mobileData.pointProgress) :
|
||||
(edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress)
|
||||
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 ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
|
||||
if (missingPoints === 0) {
|
||||
this.bot.log('SEARCH-BING', `Bing searches for ${this.bot.isMobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,33 +32,23 @@ export class Search extends Workers {
|
||||
// Deduplicate the search terms
|
||||
googleSearchQueries = [...new Set(googleSearchQueries)]
|
||||
|
||||
// Open a new tab
|
||||
const browser = page.browser()
|
||||
const searchPage = await browser.newPage()
|
||||
|
||||
// Go to bing
|
||||
await searchPage.goto(this.searchPageURL)
|
||||
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
|
||||
|
||||
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
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
const query = queries[i] as string
|
||||
|
||||
this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`)
|
||||
this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${this.bot.isMobile}`)
|
||||
|
||||
const newData = await this.bingSearch(page, searchPage, query)
|
||||
|
||||
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches
|
||||
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches
|
||||
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
|
||||
|
||||
const newMissingPoints = (mobile && newMobileData) ?
|
||||
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
|
||||
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
|
||||
searchCounters = await this.bingSearch(page, query)
|
||||
const newMissingPoints = this.calculatePoints(searchCounters)
|
||||
|
||||
// If the new point amount is the same as before
|
||||
if (newMissingPoints == missingPoints) {
|
||||
@@ -72,7 +59,13 @@ export class Search extends Workers {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -84,6 +77,11 @@ export class Search extends Workers {
|
||||
}
|
||||
}
|
||||
|
||||
// Only for mobile searches
|
||||
if (missingPoints > 0 && this.bot.isMobile) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we still got remaining search queries, generate extra ones
|
||||
if (missingPoints > 0) {
|
||||
this.bot.log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
|
||||
@@ -97,16 +95,10 @@ export class Search extends Workers {
|
||||
if (relatedTerms.length > 3) {
|
||||
// Search for the first 2 related terms
|
||||
for (const term of relatedTerms.slice(1, 3)) {
|
||||
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${mobile}`)
|
||||
const newData = await this.bingSearch(page, searchPage, query.topic)
|
||||
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`)
|
||||
|
||||
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)
|
||||
searchCounters = await this.bingSearch(page, query.topic)
|
||||
const newMissingPoints = this.calculatePoints(searchCounters)
|
||||
|
||||
// If the new point amount is the same as before
|
||||
if (newMissingPoints == missingPoints) {
|
||||
@@ -118,7 +110,7 @@ export class Search extends Workers {
|
||||
missingPoints = newMissingPoints
|
||||
|
||||
// If we satisfied the searches
|
||||
if (missingPoints == 0) {
|
||||
if (missingPoints === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -135,12 +127,12 @@ export class Search extends Workers {
|
||||
this.bot.log('SEARCH-BING', 'Completed searches')
|
||||
}
|
||||
|
||||
private async bingSearch(page: Page, searchPage: Page, query: string) {
|
||||
private async bingSearch(searchPage: Page, query: string) {
|
||||
// Try a max of 5 times
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
const searchBar = '#sb_form_q'
|
||||
await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 })
|
||||
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 })
|
||||
await searchPage.click(searchBar) // Focus on the textarea
|
||||
await this.bot.utils.wait(500)
|
||||
await searchPage.keyboard.down('Control')
|
||||
@@ -162,7 +154,7 @@ export class Search extends Workers {
|
||||
|
||||
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
|
||||
|
||||
return await this.bot.browser.func.getSearchPoints(page)
|
||||
return await this.bot.browser.func.getSearchPoints()
|
||||
|
||||
} catch (error) {
|
||||
if (i === 5) {
|
||||
@@ -173,12 +165,16 @@ export class Search extends Workers {
|
||||
this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
|
||||
this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
|
||||
|
||||
// Reset the tabs
|
||||
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||
await this.closeTabs(lastTab, this.searchPageURL)
|
||||
|
||||
await this.bot.utils.wait(4000)
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
|
||||
return await this.bot.browser.func.getSearchPoints(page)
|
||||
return await this.bot.browser.func.getSearchPoints()
|
||||
}
|
||||
|
||||
private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> {
|
||||
@@ -211,7 +207,7 @@ export class Search extends Workers {
|
||||
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
|
||||
queryTerms.push({
|
||||
topic: topic.title.query.toLowerCase(),
|
||||
related: topic.relatedQueries.map(x => x.query.toLocaleLowerCase())
|
||||
related: topic.relatedQueries.map(x => x.query.toLowerCase())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,9 +249,10 @@ export class Search extends Workers {
|
||||
private async randomScroll(page: Page) {
|
||||
try {
|
||||
// Press the arrow down key to scroll
|
||||
for (let i = 0; i < this.bot.utils.randomNumber(5, 100); i++) {
|
||||
for (let i = 0; i < this.bot.utils.randomNumber(5, 400); i++) {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
|
||||
}
|
||||
@@ -267,68 +264,61 @@ export class Search extends Workers {
|
||||
|
||||
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 this.bot.utils.wait(3000)
|
||||
|
||||
// Will get current tab if no new one is created
|
||||
let lastTab = await this.bot.browser.utils.getLatestTab(page)
|
||||
|
||||
// Wait for the body of the new page to be loaded
|
||||
await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { })
|
||||
// Let website load, if it doesn't load within 5 sec. exit regardless
|
||||
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.
|
||||
let i = 0
|
||||
while (lastTabURL.href !== searchListingURL.href && i < 5) {
|
||||
// If hostname is still bing, (Bing images/news etc)
|
||||
if (lastTabURL.hostname == searchListingURL.hostname) {
|
||||
await lastTab.goBack()
|
||||
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
|
||||
|
||||
// If "goBack" didn't return to search listing (due to redirects)
|
||||
if (lastTabURL.hostname !== searchListingURL.hostname) {
|
||||
await lastTab.goto(this.searchPageURL)
|
||||
}
|
||||
|
||||
} else { // No longer on bing, likely opened a new tab, close this tab
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
|
||||
lastTabURL = new URL(lastTab.url())
|
||||
|
||||
const tabs = await (page.browser()).pages() // Get all tabs open
|
||||
|
||||
// If the browser has more than 3 tabs open, it has opened a new one, we need to close this one.
|
||||
if (tabs.length > 3) {
|
||||
// Make sure the page is still open!
|
||||
if (!lastTab.isClosed()) {
|
||||
await lastTab.close()
|
||||
}
|
||||
|
||||
} else if (lastTabURL.href !== searchListingURL.href) {
|
||||
|
||||
await lastTab.goBack()
|
||||
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
|
||||
lastTabURL = new URL(lastTab.url())
|
||||
|
||||
// If "goBack" didn't return to search listing (due to redirects)
|
||||
if (lastTabURL.hostname !== searchListingURL.hostname) {
|
||||
await lastTab.goto(this.searchPageURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
|
||||
i++
|
||||
}
|
||||
// Check if the URL is different from the original one, don't loop more than 5 times.
|
||||
let i = 0
|
||||
while (lastTabURL.href !== searchListingURL.href && i < 5) {
|
||||
|
||||
await this.closeTabs(lastTab, searchListingURL.href)
|
||||
|
||||
// End of loop, refresh lastPage
|
||||
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
|
||||
lastTabURL = new URL(lastTab.url()) // Get new tab info
|
||||
i++
|
||||
}
|
||||
|
||||
} catch (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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
@@ -8,10 +8,10 @@ export class ThisOrThat extends Workers {
|
||||
async doThisOrThat(page: Page) {
|
||||
this.bot.log('THIS-OR-THAT', 'Trying to complete ThisOrThat')
|
||||
|
||||
return
|
||||
|
||||
try {
|
||||
// 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) {
|
||||
await page.click('#rqStartQuiz')
|
||||
} else {
|
||||
@@ -22,9 +22,22 @@ export class ThisOrThat extends Workers {
|
||||
|
||||
// Solving
|
||||
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
|
||||
|
||||
this.bot.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) {
|
||||
await page.close()
|
||||
this.bot.log('THIS-OR-THAT', 'An error occurred:' + error, 'error')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from 'puppeteer'
|
||||
import { Page } from 'playwright'
|
||||
|
||||
import { Workers } from '../Workers'
|
||||
|
||||
@@ -9,7 +9,8 @@ export class UrlReward extends Workers {
|
||||
this.bot.log('URL-REWARD', 'Trying to complete UrlReward')
|
||||
|
||||
try {
|
||||
// After waiting, close the page
|
||||
this.bot.utils.wait(2000)
|
||||
|
||||
await page.close()
|
||||
|
||||
this.bot.log('URL-REWARD', 'Completed the UrlReward successfully')
|
||||
|
||||
119
src/index.ts
119
src/index.ts
@@ -1,4 +1,5 @@
|
||||
import cluster from 'cluster'
|
||||
import { BrowserContext, Page } from 'playwright'
|
||||
|
||||
import Browser from './browser/Browser'
|
||||
import BrowserFunc from './browser/BrowserFunc'
|
||||
@@ -6,7 +7,7 @@ import BrowserUtil from './browser/BrowserUtil'
|
||||
|
||||
import { log } from './util/Logger'
|
||||
import Util from './util/Utils'
|
||||
import { loadAccounts, loadConfig } from './util/Load'
|
||||
import { loadAccounts, loadConfig, saveSessionData } from './util/Load'
|
||||
|
||||
import { Login } from './functions/Login'
|
||||
import { Workers } from './functions/Workers'
|
||||
@@ -24,6 +25,8 @@ export class MicrosoftRewardsBot {
|
||||
func: BrowserFunc,
|
||||
utils: BrowserUtil
|
||||
}
|
||||
public isMobile: boolean = false
|
||||
public homePage!: Page
|
||||
|
||||
private collectedPoints: number = 0
|
||||
private activeWorkers: number
|
||||
@@ -122,116 +125,136 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
// Desktop
|
||||
async Desktop(account: Account) {
|
||||
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, false)
|
||||
const page = 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 })
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
log('MAIN', 'Starting DESKTOP browser')
|
||||
|
||||
// Login into MS Rewards
|
||||
await this.login.login(page, account.email, account.password)
|
||||
// Login into MS Rewards, then go to rewards homepage
|
||||
await this.login.login(this.homePage, account.email, account.password)
|
||||
await this.browser.func.goHome(this.homePage)
|
||||
|
||||
const wentHome = await this.browser.func.goHome(page)
|
||||
if (!wentHome) {
|
||||
throw log('MAIN', 'Unable to get dashboard page', 'error')
|
||||
}
|
||||
|
||||
const data = await this.browser.func.getDashboardData(page)
|
||||
const data = await this.browser.func.getDashboardData()
|
||||
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
|
||||
|
||||
const earnablePoints = await this.browser.func.getEarnablePoints(data)
|
||||
const earnablePoints = await this.browser.func.getEarnablePoints()
|
||||
this.collectedPoints = earnablePoints
|
||||
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`)
|
||||
|
||||
// If runOnZeroPoints is false and 0 points to earn, don't continue
|
||||
if (!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
|
||||
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
|
||||
if (this.config.workers.doDailySet) {
|
||||
await this.workers.doDailySet(page, data)
|
||||
await this.workers.doDailySet(workerPage, data)
|
||||
}
|
||||
|
||||
// Complete more promotions
|
||||
if (this.config.workers.doMorePromotions) {
|
||||
await this.workers.doMorePromotions(page, data)
|
||||
await this.workers.doMorePromotions(workerPage, data)
|
||||
}
|
||||
|
||||
// Complete punch cards
|
||||
if (this.config.workers.doPunchCards) {
|
||||
await this.workers.doPunchCard(page, data)
|
||||
await this.workers.doPunchCard(workerPage, data)
|
||||
}
|
||||
|
||||
// Do desktop searches
|
||||
if (this.config.workers.doDesktopSearch) {
|
||||
await this.activities.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
|
||||
await browser.close()
|
||||
return await this.closeBrowser(browser, account.email)
|
||||
}
|
||||
|
||||
// Mobile
|
||||
async Mobile(account: Account) {
|
||||
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, true)
|
||||
const page = await browser.newPage()
|
||||
let pages = await browser.pages()
|
||||
this.isMobile = true
|
||||
|
||||
// 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 })
|
||||
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
|
||||
this.homePage = await browser.newPage()
|
||||
|
||||
log('MAIN', 'Starting MOBILE browser')
|
||||
|
||||
// Login into MS Rewards
|
||||
await this.login.login(page, account.email, account.password)
|
||||
// Login into MS Rewards, then go to rewards homepage
|
||||
await this.login.login(this.homePage, account.email, account.password)
|
||||
await this.browser.func.goHome(this.homePage)
|
||||
|
||||
await this.browser.func.goHome(page)
|
||||
|
||||
const data = await this.browser.func.getDashboardData(page)
|
||||
const data = await this.browser.func.getDashboardData()
|
||||
|
||||
// If no mobile searches data found, stop (Does not exist on new accounts)
|
||||
if (!data.userStatus.counters.mobileSearch) {
|
||||
log('MAIN', 'No mobile searches found, stopping')
|
||||
log('MAIN', 'No mobile searches found, stopping!')
|
||||
|
||||
// Close mobile browser
|
||||
return await browser.close()
|
||||
return await this.closeBrowser(browser, account.email)
|
||||
}
|
||||
|
||||
// Open a new tab to where the tasks are going to be completed
|
||||
const workerPage = await browser.newPage()
|
||||
|
||||
// Go to homepage on worker page
|
||||
await this.browser.func.goHome(workerPage)
|
||||
|
||||
// Do mobile searches
|
||||
if (this.config.workers.doMobileSearch) {
|
||||
await this.activities.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
|
||||
const earnablePoints = await this.browser.func.getEarnablePoints(data, page)
|
||||
const earnablePoints = await this.browser.func.getEarnablePoints()
|
||||
|
||||
// If the new earnable is 0, means we got all the points, else retract
|
||||
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)
|
||||
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
|
||||
|
||||
// Close mobile browser
|
||||
return await this.closeBrowser(browser, account.email)
|
||||
}
|
||||
|
||||
private async closeBrowser(browser: BrowserContext, email: string) {
|
||||
// Save cookies
|
||||
const cookies = await browser.cookies()
|
||||
await saveSessionData(this.config.sessionPath, email, this.isMobile, cookies)
|
||||
|
||||
// Close browser
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const bot = new MicrosoftRewardsBot()
|
||||
|
||||
// Initialize accounts first and then start the bot
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface Config {
|
||||
workers: Workers;
|
||||
searchSettings: SearchSettings;
|
||||
webhook: Webhook;
|
||||
saveFingerprint: boolean;
|
||||
}
|
||||
|
||||
export interface SearchSettings {
|
||||
@@ -14,6 +15,7 @@ export interface SearchSettings {
|
||||
scrollRandomResults: boolean;
|
||||
clickRandomResults: boolean;
|
||||
searchDelay: SearchDelay;
|
||||
retryMobileSearch: boolean;
|
||||
}
|
||||
|
||||
export interface SearchDelay {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Cookie } from 'playwright'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config } from '../interface/Config'
|
||||
|
||||
@@ -32,4 +35,72 @@ export function loadConfig(): 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)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Webhook } from './Webhook'
|
||||
|
||||
export function log(title: string, message: string, type?: 'log' | 'warn' | 'error') {
|
||||
const currentTime = new Date().toISOString()
|
||||
const currentTime = new Date().toLocaleString()
|
||||
|
||||
let str = ''
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function getEdgeVersions() {
|
||||
|
||||
export function getSystemComponents(mobile: boolean): string {
|
||||
const osId: string = mobile ? 'Linux' : 'Windows NT 10.0'
|
||||
const uaPlatform: string = mobile ? 'Android 13' : 'Win64; x64'
|
||||
const uaPlatform: string = mobile ? `Android 1${Math.floor(Math.random() * 5)}` : 'Win64; x64'
|
||||
|
||||
if (mobile) {
|
||||
return `${uaPlatform}; ${osId}; K`
|
||||
|
||||
Reference in New Issue
Block a user