mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-18 05:53:57 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c70d6f9cb1 | ||
|
|
a47b86e74d | ||
|
|
ce2a72ee36 | ||
|
|
755237caa1 | ||
|
|
2b4cd505c0 | ||
|
|
a39a861dab | ||
|
|
8d19129906 | ||
|
|
c6ab80fe54 | ||
|
|
9b1eed526f | ||
|
|
9a144b2e60 | ||
|
|
28b1881642 | ||
|
|
ef6ad569ff | ||
|
|
da9ba91c5c | ||
|
|
deb2d58b1b | ||
|
|
66a82c2584 | ||
|
|
8a022d5983 | ||
|
|
64048e35d7 | ||
|
|
cf7f7ac790 | ||
|
|
f7aa5039f9 | ||
|
|
e082fb03f0 | ||
|
|
0303b8c605 | ||
|
|
2fea17c415 | ||
|
|
c5beccb54b | ||
|
|
b566ccaece | ||
|
|
15b2b827eb | ||
|
|
02518ee4ba | ||
|
|
69819b5631 | ||
|
|
b389b87792 | ||
|
|
9ea7f5c452 | ||
|
|
f3fb641ecd | ||
|
|
bca1e7c896 | ||
|
|
fd7c8e36d4 | ||
|
|
dcb0c25d46 | ||
|
|
a8cb5482d4 | ||
|
|
28286ff9fe | ||
|
|
1d6167aeca | ||
|
|
3b15fe19a7 | ||
|
|
66dcdd4a7a | ||
|
|
40f32af67f | ||
|
|
c2b68faa74 |
@@ -28,6 +28,10 @@ module.exports = {
|
|||||||
'error',
|
'error',
|
||||||
'never'
|
'never'
|
||||||
],
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any':
|
||||||
|
['warn', {
|
||||||
|
fixToUnknown: true // This line is optional and only relevant if you are using TypeScript
|
||||||
|
}],
|
||||||
'comma-dangle': 'off',
|
'comma-dangle': 'off',
|
||||||
'@typescript-eslint/comma-dangle': 'error',
|
'@typescript-eslint/comma-dangle': 'error',
|
||||||
'prefer-arrow-callback': 'error'
|
'prefer-arrow-callback': 'error'
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@ package-lock.json
|
|||||||
accounts.json
|
accounts.json
|
||||||
notes
|
notes
|
||||||
accounts.dev.json
|
accounts.dev.json
|
||||||
accounts.main.json
|
accounts.main.json
|
||||||
|
.DS_Store
|
||||||
|
|||||||
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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Use an official Node.js runtime as a base image
|
||||||
|
FROM node:18
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /usr/src/microsoft-rewards-script
|
||||||
|
|
||||||
|
# Install jq, cron, and gettext-base
|
||||||
|
RUN apt-get update && apt-get install -y jq cron gettext-base
|
||||||
|
|
||||||
|
# Copy all files to the working directory
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install dependencies including Playwright
|
||||||
|
RUN apt-get install -y \
|
||||||
|
xvfb \
|
||||||
|
libgbm-dev \
|
||||||
|
libnss3 \
|
||||||
|
libasound2 \
|
||||||
|
libxss1 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install application dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Build the script
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Install playwright chromium
|
||||||
|
RUN npx playwright install chromium
|
||||||
|
|
||||||
|
# Copy cron file to cron directory
|
||||||
|
COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
|
||||||
|
|
||||||
|
# Create the log file to be able to run tail
|
||||||
|
RUN touch /var/log/cron.log
|
||||||
|
|
||||||
|
# Define the command to run your application with cron optionally
|
||||||
|
CMD sh -c 'node src/updateConfig.js && echo "$TZ" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata && if [ "$RUN_ON_START" = "true" ]; then npm start; fi && envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && crontab /etc/cron.d/microsoft-rewards-cron && cron && tail -f /var/log/cron.log'
|
||||||
78
README.md
78
README.md
@@ -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,10 +11,79 @@ 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 kill-chrome-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.
|
||||||
|
|
||||||
|
## Docker (Experimental) ##
|
||||||
|
**Note:** If you had previously built and run the script locally, remove the `/node_modules` and `/dist` from your Microsoft-Rewards-Script folder.
|
||||||
|
|
||||||
|
1. Download the source code
|
||||||
|
2. Make changes to your `accounts.json` and `config.json`
|
||||||
|
3. **Headless mode must be enabled.** You can do this in `config.json` or by using the `HEADLESS=true` environmental variable in docker run or docker compose.yaml (see below). Environmental variables are prioritized over the values in config.json.
|
||||||
|
4. The container has in-built scheduling. Customize your schedule using the `CRON_START_TIME` environmental variable. Use [crontab.guru](crontab.guru) if you're unsure how to create a cron schedule.
|
||||||
|
5. **Note:** the container will add between 5 and 50 minutes of randomized variability to your scheduled start times.
|
||||||
|
|
||||||
|
### Option 1: build and run with docker run
|
||||||
|
|
||||||
|
1. Build or re-build the container image with: `docker build -t microsoft-rewards-script-docker .`
|
||||||
|
|
||||||
|
2. Run the container with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name netsky -d \
|
||||||
|
-e TZ=America/New_York \
|
||||||
|
-e HEADLESS=true \
|
||||||
|
-e RUN_ON_START=true \
|
||||||
|
-e CRON_START_TIME="0 5,11 * * *" \
|
||||||
|
microsoft-rewards-script-docker
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Optionally, customize your config by adding any other environmental variables from the table below.
|
||||||
|
|
||||||
|
4. You can view logs with `docker logs netsky`.
|
||||||
|
|
||||||
|
### Option 2: use docker compose
|
||||||
|
|
||||||
|
1. A basic docker compose.yaml has been provided.
|
||||||
|
|
||||||
|
2. Optionally, customize your config by adding any other environmental variables from the table below.
|
||||||
|
|
||||||
|
3. Build and start the container using `docker compose up -d`.
|
||||||
|
|
||||||
|
4. You can view logs with `docker logs netsky`
|
||||||
|
|
||||||
|
|
||||||
|
## Config ##
|
||||||
|
| Setting | Description | Default | Docker Environmental Variable |
|
||||||
|
| :------------- |:-------------| :-----| :-----|
|
||||||
|
| baseURL | MS Rewards page | `https://rewards.bing.com` | BASE_URL |
|
||||||
|
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) | SESSION_PATH |
|
||||||
|
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) | HEADLESS *(must be set to `=true` for docker)* |
|
||||||
|
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | RUN_ON_ZERO_POINTS |
|
||||||
|
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) | CLUSTERS |
|
||||||
|
| saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | SAVE_FINGERPRINT |
|
||||||
|
| workers.doDailySet | Complete daily set items | `true` | DO_DAILY_SET |
|
||||||
|
| workers.doMorePromotions | Complete promotional items | `true` | DO_MORE_PROMOTIONS |
|
||||||
|
| workers.doPunchCards | Complete punchcards | `true` | DO_PUNCH_CARDS |
|
||||||
|
| workers.doDesktopSearch | Complete daily desktop searches | `true` | DO_DESKTOP_SEARCH |
|
||||||
|
| workers.doMobileSearch | Complete daily mobile searches | `true` | DO_MOBILE_SEARCH |
|
||||||
|
| globalTimeout | The length before the action gets timeout | `30000` (30 seconds) | GLOBAL_TIMEOUT |
|
||||||
|
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | USE_GEO_LOCALE_QUERIES |
|
||||||
|
| scrollRandomResults | Scroll randomly in search results | `true` | SCROLL_RANDOM_RESULTS |
|
||||||
|
| searchSettings.clickRandomResults | Visit random website from search result| `true` | CLICK_RANDOM_RESULTS |
|
||||||
|
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 10000` (10 seconds) `max: 20000` (20 seconds) | SEARCH_DELAY_MIN SEARCH_DELAY_MAX |
|
||||||
|
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` | RETRY_MOBILE_SEARCH |
|
||||||
|
| webhook.enabled | Enable or disable your set webhook | `false` | WEBHOOK_ENABLED |
|
||||||
|
| webhook.url | Your Discord webhook URL | `null` | WEBHOOK_URL="" |
|
||||||
|
| cronStartTime | Scheduled script run-time, *only available for docker implementation* | `0 5,11 * * *` (5:00 am, 11:00 am daily) | CRON_START_TIME="" |
|
||||||
|
| | Run the script immediately when the Docker container starts | `true` | RUN_ON_START |
|
||||||
|
|
||||||
## Features ##
|
## Features ##
|
||||||
- [x] Multi-Account Support
|
- [x] Multi-Account Support
|
||||||
- [x] Session Storing
|
- [x] Session Storing
|
||||||
- [x] 2FA Support
|
- [x] 2FA Support
|
||||||
|
- [x] Passwordless Support
|
||||||
- [x] Headless Support
|
- [x] Headless Support
|
||||||
- [x] Discord Webhook Support
|
- [x] Discord Webhook Support
|
||||||
- [x] Desktop Searches
|
- [x] Desktop Searches
|
||||||
@@ -31,13 +100,16 @@ 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
|
||||||
|
- [x] Completing Daily Check In
|
||||||
|
- [x] Completing Read To Earn
|
||||||
- [ ] Completing Shopping Game
|
- [ ] Completing Shopping Game
|
||||||
- [ ] Completing Gaming Tab
|
- [ ] Completing Gaming Tab
|
||||||
- [x] Clustering Support
|
- [x] Clustering Support
|
||||||
- [x] Proxy Support
|
- [x] Proxy Support
|
||||||
|
- [x] Docker Support (experimental)
|
||||||
|
- [x] Automatic scheduling (via Docker)
|
||||||
|
|
||||||
## Disclaimer ##
|
## Disclaimer ##
|
||||||
Your account may be at risk of getting banned or suspended using this script, you've been warned!
|
Your account may be at risk of getting banned or suspended using this script, you've been warned!
|
||||||
|
|||||||
13
compose.yaml
Normal file
13
compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
microsoft-rewards-script:
|
||||||
|
build: .
|
||||||
|
container_name: netsky
|
||||||
|
environment:
|
||||||
|
- TZ=America/Toronto #change to your local timezone
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HEADLESS=true #do not change
|
||||||
|
### Customize your run schedule, default 5:00 am and 11:00 am, use crontab.guru for formatting
|
||||||
|
- CRON_START_TIME=0 5,11 * * *
|
||||||
|
### Run on start, set to false to only run the script per the cron schedule
|
||||||
|
- RUN_ON_START=true
|
||||||
|
restart: unless-stopped
|
||||||
30
package.json
30
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "microsoft-rewards-script",
|
"name": "microsoft-rewards-script",
|
||||||
"version": "1.2.1",
|
"version": "1.4.9",
|
||||||
"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,9 @@
|
|||||||
"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 }\"",
|
||||||
|
"create-docker": "docker build -t microsoft-rewards-script-docker ."
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Bing Rewards",
|
"Bing Rewards",
|
||||||
@@ -18,23 +20,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": "^7.17.0",
|
||||||
"typescript": "^5.2.2"
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-modules-newline": "^0.0.6",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.7.5",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0",
|
||||||
"eslint": "^8.49.0",
|
"fingerprint-generator": "^2.1.54",
|
||||||
"eslint-plugin-modules-newline": "^0.0.6",
|
"fingerprint-injector": "^2.1.54",
|
||||||
"puppeteer": "^21.2.1",
|
"playwright": "^1.46.1",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"ts-node": "^10.9.2"
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
||||||
"ts-node": "^10.9.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,71 @@
|
|||||||
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 })
|
||||||
|
|
||||||
|
// Set timeout to preferred amount
|
||||||
|
context.setDefaultTimeout(this.bot.config?.globalTimeout ?? 30_000)
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Browser
|
export default Browser
|
||||||
|
|||||||
@@ -1,248 +1,320 @@
|
|||||||
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 axios, { AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
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 { AppUserData } from '../interface/AppUserData'
|
||||||
|
|
||||||
import { baseURL, sessionPath } from './../config.json'
|
|
||||||
|
|
||||||
export async function goHome(page: Page): Promise<boolean> {
|
export default class BrowserFunc {
|
||||||
|
private bot: MicrosoftRewardsBot
|
||||||
|
|
||||||
try {
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
const dashboardURL = new URL(baseURL)
|
this.bot = bot
|
||||||
|
|
||||||
await page.goto(baseURL)
|
|
||||||
|
|
||||||
const maxIterations = 5 // Maximum iterations set to 5
|
|
||||||
|
|
||||||
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
||||||
await wait(3000)
|
|
||||||
await tryDismissCookieBanner(page)
|
|
||||||
|
|
||||||
// Check if account is suspended
|
|
||||||
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
|
|
||||||
if (isSuspended) {
|
|
||||||
log('GO-HOME', 'This account is suspended!', 'error')
|
|
||||||
throw new Error('Account has been suspended!')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If activities are found, exit the loop
|
|
||||||
await page.waitForSelector('#more-activities', { timeout: 1000 })
|
|
||||||
break
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Continue if element is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentURL = new URL(page.url())
|
|
||||||
|
|
||||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
|
||||||
await tryDismissAllMessages(page)
|
|
||||||
|
|
||||||
await wait(2000)
|
|
||||||
await page.goto(baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(5000)
|
|
||||||
log('GO-HOME', 'Visited homepage successfully')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('An error occurred:', error)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDashboardData(page: Page): Promise<DashboardData> {
|
/**
|
||||||
const dashboardURL = new URL(baseURL)
|
* Navigate the provided page to rewards homepage
|
||||||
const currentURL = new URL(page.url())
|
* @param {Page} page Playwright page
|
||||||
|
*/
|
||||||
|
async goHome(page: Page) {
|
||||||
|
|
||||||
// Should never happen since tasks are opened in a new tab!
|
try {
|
||||||
if (currentURL.hostname !== dashboardURL.hostname) {
|
const dashboardURL = new URL(this.bot.config.baseURL)
|
||||||
log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
|
||||||
await goHome(page)
|
if (page.url() === dashboardURL.href) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto(this.bot.config.baseURL)
|
||||||
|
|
||||||
|
const maxIterations = 5 // Maximum iterations set to 5
|
||||||
|
|
||||||
|
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||||
|
await this.bot.utils.wait(3000)
|
||||||
|
await this.bot.browser.utils.tryDismissCookieBanner(page)
|
||||||
|
|
||||||
|
// Check if account is suspended
|
||||||
|
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { 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!')
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw this.bot.log('GO-HOME', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the page to get new data
|
/**
|
||||||
await page.reload({ waitUntil: 'networkidle2' })
|
* 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(this.bot.homePage.url())
|
||||||
|
|
||||||
const scriptContent = await page.evaluate(() => {
|
// Should never happen since tasks are opened in a new tab!
|
||||||
const scripts = Array.from(document.querySelectorAll('script'))
|
if (currentURL.hostname !== dashboardURL.hostname) {
|
||||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
this.bot.log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||||
|
await this.goHome(this.bot.homePage)
|
||||||
if (targetScript) {
|
|
||||||
return targetScript.innerText
|
|
||||||
} else {
|
|
||||||
throw log('GET-DASHBOARD-DATA', 'Script containing dashboard data not found', 'error')
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// Extract the dashboard object from the script content
|
// Reload the page to get new data
|
||||||
const dashboardData = await page.evaluate(scriptContent => {
|
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
|
||||||
// Extract the dashboard object using regex
|
|
||||||
const regex = /var dashboard = (\{.*?\});/s
|
|
||||||
const match = regex.exec(scriptContent)
|
|
||||||
|
|
||||||
if (match && match[1]) {
|
const scriptContent = await this.bot.homePage.evaluate(() => {
|
||||||
return JSON.parse(match[1])
|
const scripts = Array.from(document.querySelectorAll('script'))
|
||||||
} else {
|
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||||
throw log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
|
||||||
|
return targetScript?.innerText ? targetScript.innerText : null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!scriptContent) {
|
||||||
|
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
||||||
}
|
}
|
||||||
}, scriptContent)
|
|
||||||
|
|
||||||
return dashboardData
|
// Extract the dashboard object from the script content
|
||||||
}
|
const dashboardData = await this.bot.homePage.evaluate(scriptContent => {
|
||||||
|
// Extract the dashboard object using regex
|
||||||
export async function getQuizData(page: Page): Promise<QuizData> {
|
const regex = /var dashboard = (\{.*?\});/s
|
||||||
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)
|
const match = regex.exec(scriptContent)
|
||||||
|
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
const quizData = JSON.parse(match[1])
|
return JSON.parse(match[1])
|
||||||
return quizData
|
}
|
||||||
|
}, scriptContent)
|
||||||
|
|
||||||
|
if (!dashboardData) {
|
||||||
|
throw this.bot.log('GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboardData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 with web browser
|
||||||
|
* @returns {number} Total earnable points
|
||||||
|
*/
|
||||||
|
async getBrowserEarnablePoints(): 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
|
||||||
|
if (data.userStatus.counters.pcSearch?.length) {
|
||||||
|
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
|
||||||
|
if (data.morePromotions?.length) {
|
||||||
|
data.morePromotions.forEach(x => {
|
||||||
|
// Only count points from supported activities
|
||||||
|
if (['quiz', 'urlreward'].includes(x.promotionType) && !x.attributes.is_unlocked) {
|
||||||
|
totalEarnablePoints += (x.pointProgressMax - x.pointProgress)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalEarnablePoints
|
||||||
|
} catch (error) {
|
||||||
|
throw this.bot.log('GET-BROWSER-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total earnable points with mobile app
|
||||||
|
* @returns {number} Total earnable points
|
||||||
|
*/
|
||||||
|
async getAppEarnablePoints(accessToken: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const eligibleOffers = [
|
||||||
|
'ENUS_readarticle3_30points',
|
||||||
|
'Gamification_Sapphire_DailyCheckIn'
|
||||||
|
]
|
||||||
|
let totalEarnablePoints = 0
|
||||||
|
|
||||||
|
const data = await this.getDashboardData()
|
||||||
|
let geoLocale = data.userProfile.attributes.country
|
||||||
|
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||||
|
|
||||||
|
const userDataRequest: AxiosRequestConfig = {
|
||||||
|
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'X-Rewards-Country': geoLocale,
|
||||||
|
'X-Rewards-Language': 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataResponse: AppUserData = (await axios(userDataRequest)).data
|
||||||
|
const userData = userDataResponse.response
|
||||||
|
const eligibleActivities = userData.promotions.filter((x) => eligibleOffers.includes(x.attributes.offerid ?? ''))
|
||||||
|
|
||||||
|
for (const item of eligibleActivities) {
|
||||||
|
if (item.attributes.type === 'msnreadearn') {
|
||||||
|
totalEarnablePoints += parseInt(item.attributes.pointmax ?? '') - parseInt(item.attributes.pointprogress ?? '')
|
||||||
|
break
|
||||||
|
} else if (item.attributes.type === 'checkin') {
|
||||||
|
const checkInDay = parseInt(item.attributes.progress ?? '') % 7
|
||||||
|
|
||||||
|
if (checkInDay < 6 && (new Date()).getDate() != (new Date(item.attributes.last_updated ?? '')).getDate()) {
|
||||||
|
totalEarnablePoints += parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalEarnablePoints
|
||||||
|
} catch (error) {
|
||||||
|
throw this.bot.log('GET-APP-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 {
|
||||||
|
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 {
|
} else {
|
||||||
throw log('GET-QUIZ-DATA', 'Quiz data not found within script', 'error')
|
throw this.bot.log('GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw 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')
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw log('GET-QUIZ-DATA', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
async waitForQuizRefresh(page: Page): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 })
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
|
||||||
export async function getSearchPoints(page: Page): Promise<Counters> {
|
return true
|
||||||
const dashboardData = await getDashboardData(page) // Always fetch newest data
|
} catch (error) {
|
||||||
|
this.bot.log('QUIZ-REFRESH', 'An error occurred:' + error, 'error')
|
||||||
return dashboardData.userStatus.counters
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEarnablePoints(data: DashboardData, page: null | Page = null): Promise<number> {
|
|
||||||
try {
|
|
||||||
// Fetch new data if page is provided
|
|
||||||
if (page) {
|
|
||||||
data = await getDashboardData(page)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// These only include the points from tasks that the script can complete!
|
async checkQuizCompleted(page: Page): Promise<boolean> {
|
||||||
let totalEarnablePoints = 0
|
try {
|
||||||
|
await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 })
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
|
||||||
// Desktop Search Points
|
return true
|
||||||
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
|
} catch (error) {
|
||||||
|
return false
|
||||||
// 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> {
|
async refreshCheerio(page: Page): Promise<CheerioAPI> {
|
||||||
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
|
|
||||||
} catch (error) {
|
|
||||||
log('QUIZ-REFRESH', 'An error occurred:' + error, 'error')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkQuizCompleted(page: Page): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 1000 })
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function refreshCheerio(page: Page): Promise<CheerioAPI> {
|
|
||||||
const html = await page.content()
|
|
||||||
const $ = load(html)
|
|
||||||
|
|
||||||
return $
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise<string> {
|
|
||||||
let selector = ''
|
|
||||||
try {
|
|
||||||
const html = await page.content()
|
const html = await page.content()
|
||||||
const $ = load(html)
|
const $ = load(html)
|
||||||
|
|
||||||
const element = $('.offer-cta').toArray().find(x => x.attribs.href?.includes(activity.offerId))
|
return $
|
||||||
if (element) {
|
}
|
||||||
selector = `a[href*="${element.attribs.href}"]`
|
|
||||||
}
|
async getPunchCardActivity(page: Page, activity: PromotionalItem | MorePromotion): Promise<string> {
|
||||||
} catch (error) {
|
let selector = ''
|
||||||
log('GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error')
|
try {
|
||||||
|
const html = await page.content()
|
||||||
|
const $ = load(html)
|
||||||
|
|
||||||
|
const element = $('.offer-cta').toArray().find(x => x.attribs.href?.includes(activity.offerId))
|
||||||
|
if (element) {
|
||||||
|
selector = `a[href*="${element.attribs.href}"]`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('GET-PUNCHCARD-ACTIVITY', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return selector
|
||||||
}
|
}
|
||||||
|
|
||||||
return selector
|
|
||||||
}
|
}
|
||||||
@@ -1,81 +1,125 @@
|
|||||||
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> {
|
|
||||||
const buttons = [
|
|
||||||
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
|
|
||||||
{ selector: '#iShowSkip', label: 'iShowSkip' },
|
|
||||||
{ selector: '#iNext', label: 'iNext' },
|
|
||||||
{ selector: '#iLooksGood', label: 'iLooksGood' },
|
|
||||||
{ selector: '#idSIButton9', label: 'idSIButton9' },
|
|
||||||
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' }
|
|
||||||
]
|
|
||||||
|
|
||||||
let result = false
|
export default class BrowserUtil {
|
||||||
|
private bot: MicrosoftRewardsBot
|
||||||
|
|
||||||
for (const button of buttons) {
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
|
this.bot = bot
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryDismissAllMessages(page: Page): Promise<boolean> {
|
||||||
|
const buttons = [
|
||||||
|
{ selector: '#acceptButton', label: 'AcceptButton' },
|
||||||
|
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
|
||||||
|
{ selector: '#iShowSkip', label: 'iShowSkip' },
|
||||||
|
{ selector: '#iNext', label: 'iNext' },
|
||||||
|
{ selector: '#iLooksGood', label: 'iLooksGood' },
|
||||||
|
{ selector: '#idSIButton9', label: 'idSIButton9' },
|
||||||
|
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' }
|
||||||
|
]
|
||||||
|
|
||||||
|
let result = false
|
||||||
|
|
||||||
|
for (const button of buttons) {
|
||||||
|
try {
|
||||||
|
const element = await page.waitForSelector(button.selector, { state: 'visible', timeout: 1000 })
|
||||||
|
if (element) {
|
||||||
|
await element.click()
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryDismissCookieBanner(page: Page): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 })
|
await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 })
|
||||||
if (element) {
|
const cookieBanner = await page.$('#cookieConsentContainer')
|
||||||
await element.click()
|
|
||||||
result = true
|
if (cookieBanner) {
|
||||||
|
const button = await cookieBanner.$('button')
|
||||||
|
if (button) {
|
||||||
|
await button.click()
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
continue
|
// Continue if element is not found or other error occurs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
async tryDismissBingCookieBanner(page: Page): Promise<void> {
|
||||||
}
|
try {
|
||||||
|
await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 })
|
||||||
|
const cookieBanner = await page.$('#bnp_btn_accept')
|
||||||
|
|
||||||
export async function tryDismissCookieBanner(page: Page): Promise<void> {
|
if (cookieBanner) {
|
||||||
try {
|
await cookieBanner.click()
|
||||||
await page.waitForSelector('#cookieConsentContainer', { timeout: 1000 })
|
|
||||||
const cookieBanner = await page.$('#cookieConsentContainer')
|
|
||||||
|
|
||||||
if (cookieBanner) {
|
|
||||||
const button = await cookieBanner.$('button')
|
|
||||||
if (button) {
|
|
||||||
await button.click()
|
|
||||||
await wait(2000)
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue if element is not found or other error occurs
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Continue if element is not found or other error occurs
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function tryDismissBingCookieBanner(page: Page): Promise<void> {
|
async getLatestTab(page: Page): Promise<Page> {
|
||||||
try {
|
try {
|
||||||
await page.waitForSelector('#bnp_btn_accept', { timeout: 1000 })
|
await this.bot.utils.wait(500)
|
||||||
const cookieBanner = await page.$('#bnp_btn_accept')
|
|
||||||
|
|
||||||
if (cookieBanner) {
|
const browser = page.context()
|
||||||
await cookieBanner.click()
|
const pages = browser.pages()
|
||||||
|
const newTab = pages[pages.length - 1]
|
||||||
|
|
||||||
|
if (newTab) {
|
||||||
|
return newTab
|
||||||
|
}
|
||||||
|
|
||||||
|
throw this.bot.log('GET-NEW-TAB', 'Unable to get latest tab', 'error')
|
||||||
|
} catch (error) {
|
||||||
|
throw this.bot.log('GET-NEW-TAB', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Continue if element is not found or other error occurs
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLatestTab(page: Page): Promise<Page> {
|
async getTabs(page: Page) {
|
||||||
try {
|
try {
|
||||||
await wait(500)
|
const browser = page.context()
|
||||||
|
const pages = browser.pages()
|
||||||
|
|
||||||
const browser = page.browser()
|
const homeTab = pages[1]
|
||||||
const pages = await browser.pages()
|
let homeTabURL: URL
|
||||||
const newTab = pages[pages.length - 1]
|
|
||||||
|
|
||||||
if (newTab) {
|
if (!homeTab) {
|
||||||
return newTab
|
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
throw log('GET-NEW-TAB', 'Unable to get latest tab', 'error')
|
|
||||||
} catch (error) {
|
|
||||||
throw log('GET-NEW-TAB', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,32 @@
|
|||||||
"baseURL": "https://rewards.bing.com",
|
"baseURL": "https://rewards.bing.com",
|
||||||
"sessionPath": "sessions",
|
"sessionPath": "sessions",
|
||||||
"headless": false,
|
"headless": false,
|
||||||
"runOnZeroPoints": true,
|
"runOnZeroPoints": false,
|
||||||
"clusters": 1,
|
"clusters": 1,
|
||||||
|
"saveFingerprint": false,
|
||||||
"workers": {
|
"workers": {
|
||||||
"doDailySet": true,
|
"doDailySet": true,
|
||||||
"doMorePromotions": true,
|
"doMorePromotions": true,
|
||||||
"doPunchCards": true,
|
"doPunchCards": true,
|
||||||
"doDesktopSearch": true,
|
"doDesktopSearch": true,
|
||||||
"doMobileSearch": true
|
"doMobileSearch": true,
|
||||||
|
"doDailyCheckIn": true,
|
||||||
|
"doReadToEarn": true
|
||||||
},
|
},
|
||||||
|
"globalTimeout": 30000,
|
||||||
"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,
|
||||||
"url": ""
|
"url": ""
|
||||||
}
|
},
|
||||||
|
"cronStartTime": "0 5,11 * * *"
|
||||||
}
|
}
|
||||||
1
src/crontab.template
Normal file
1
src/crontab.template
Normal file
@@ -0,0 +1 @@
|
|||||||
|
${CRON_START_TIME} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /var/log/cron.log 2>&1
|
||||||
64
src/functions/Activities.ts
Normal file
64
src/functions/Activities.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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 { ReadToEarn } from './activities/ReadToEarn'
|
||||||
|
import { DailyCheckIn } from './activities/DailyCheckIn'
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
doReadToEarn = async (accessToken: string, data: DashboardData): Promise<void> => {
|
||||||
|
const readToEarn = new ReadToEarn(this.bot)
|
||||||
|
await readToEarn.doReadToEarn(accessToken, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
doDailyCheckIn = async (accessToken: string, data: DashboardData): Promise<void> => {
|
||||||
|
const dailyCheckIn = new DailyCheckIn(this.bot)
|
||||||
|
await dailyCheckIn.doDailyCheckIn(accessToken, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,65 +1,144 @@
|
|||||||
import { Page } from 'puppeteer'
|
import { Page } from 'playwright'
|
||||||
import readline from 'readline'
|
import readline from 'readline'
|
||||||
|
|
||||||
|
import { MicrosoftRewardsBot } from '../index'
|
||||||
|
import { saveSessionData } from '../util/Load'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { OAuth } from '../interface/OAuth'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
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
|
||||||
|
private clientId: string = '0000000040170455'
|
||||||
|
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
|
||||||
|
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
|
||||||
|
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
|
||||||
|
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
||||||
|
|
||||||
try {
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
// Navigate to the Bing login page
|
this.bot = bot
|
||||||
await page.goto('https://login.live.com/')
|
}
|
||||||
|
|
||||||
const isLoggedIn = await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 5000 }).then(() => true).catch(() => false)
|
async login(page: Page, email: string, password: string) {
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
try {
|
||||||
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 5000 }).then(() => true).catch(() => false)
|
// Navigate to the Bing login page
|
||||||
if (isLocked) {
|
await page.goto('https://rewards.bing.com/signin')
|
||||||
log('LOGIN', 'This account has been locked!', 'error')
|
|
||||||
throw new Error('Account has been locked!')
|
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
// Check if account is locked
|
||||||
|
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 1000 }).then(() => true).catch(() => false)
|
||||||
|
if (isLocked) {
|
||||||
|
this.bot.log('LOGIN', 'This account has been locked!', 'error')
|
||||||
|
throw new Error('Account has been locked!')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.execLogin(page, email, password)
|
||||||
|
this.bot.log('LOGIN', 'Logged into Microsoft successfully')
|
||||||
|
} else {
|
||||||
|
this.bot.log('LOGIN', 'Already logged in')
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 })
|
// Check if logged in to bing
|
||||||
|
await this.checkBingLogin(page)
|
||||||
|
|
||||||
await execLogin(page, email, password)
|
// Save session
|
||||||
log('LOGIN', 'Logged into Microsoft successfully')
|
await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile)
|
||||||
} else {
|
|
||||||
log('LOGIN', 'Already logged in')
|
// We're done logging in
|
||||||
|
this.bot.log('LOGIN', 'Logged in successfully')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Throw and don't continue
|
||||||
|
throw this.bot.log('LOGIN', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if logged in to bing
|
|
||||||
await checkBingLogin(page)
|
|
||||||
|
|
||||||
// We're done logging in
|
|
||||||
log('LOGIN', 'Logged in successfully')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Throw and don't continue
|
|
||||||
throw log('LOGIN', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function execLogin(page: Page, email: string, password: string) {
|
private async execLogin(page: Page, email: string, password: string) {
|
||||||
await page.type('#i0116', email)
|
try {
|
||||||
await page.click('#idSIButton9')
|
await this.enterEmail(page, email)
|
||||||
|
await this.enterPassword(page, password)
|
||||||
|
await this.checkLoggedIn(page)
|
||||||
|
} catch (error: any) {
|
||||||
|
this.bot.log('LOGIN', 'An error occurred: ' + error.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log('LOGIN', 'Email entered successfully')
|
private async enterEmail(page: Page, email: string) {
|
||||||
|
await page.fill('#i0116', email)
|
||||||
try {
|
|
||||||
await page.waitForSelector('#i0118', { visible: true, timeout: 2000 })
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
await page.type('#i0118', password)
|
|
||||||
await page.click('#idSIButton9')
|
await page.click('#idSIButton9')
|
||||||
|
this.bot.log('LOGIN', 'Email entered successfully')
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
private async enterPassword(page: Page, password: string) {
|
||||||
log('LOGIN', '2FA code required')
|
try {
|
||||||
|
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
await page.fill('#i0118', password)
|
||||||
|
await page.click('#idSIButton9')
|
||||||
|
this.bot.log('LOGIN', 'Password entered successfully')
|
||||||
|
} catch {
|
||||||
|
this.bot.log('LOGIN', 'Password entry failed or 2FA required')
|
||||||
|
await this.handle2FA(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handle2FA(page: Page) {
|
||||||
|
try {
|
||||||
|
const numberToPress = await this.get2FACode(page)
|
||||||
|
if (numberToPress) {
|
||||||
|
// Authentictor App verification
|
||||||
|
await this.authAppVerification(page, numberToPress)
|
||||||
|
} else {
|
||||||
|
// SMS verification
|
||||||
|
await this.authSMSVerification(page)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.bot.log('LOGIN', `2FA handling failed: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async get2FACode(page: Page): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
|
||||||
|
return await element.textContent()
|
||||||
|
} catch {
|
||||||
|
await page.click('button[aria-describedby="confirmSendTitle"]')
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
|
||||||
|
return await element.textContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authAppVerification(page: Page, numberToPress: string | null) {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
this.bot.log('LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
|
||||||
|
this.bot.log('LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
|
||||||
|
|
||||||
|
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
|
||||||
|
|
||||||
|
this.bot.log('LOGIN', 'Login successfully approved!')
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
this.bot.log('LOGIN', 'The code is expired. Trying to get a new code...')
|
||||||
|
await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]')
|
||||||
|
numberToPress = await this.get2FACode(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authSMSVerification(page: Page) {
|
||||||
|
this.bot.log('LOGIN', 'SMS 2FA code required. Waiting 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) => {
|
||||||
@@ -68,58 +147,111 @@ async function execLogin(page: Page, email: string, password: string) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.type('input[name="otc"]', code)
|
await page.fill('input[name="otc"]', code)
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
|
this.bot.log('LOGIN', '2FA code entered successfully')
|
||||||
} finally {
|
|
||||||
log('LOGIN', 'Password entered successfully')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentURL = new URL(page.url())
|
private async checkLoggedIn(page: Page) {
|
||||||
|
const targetHostname = 'rewards.bing.com'
|
||||||
|
const targetPathname = '/'
|
||||||
|
|
||||||
while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') {
|
// eslint-disable-next-line no-constant-condition
|
||||||
await tryDismissAllMessages(page)
|
while (true) {
|
||||||
currentURL.href = page.url()
|
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||||
}
|
const currentURL = new URL(page.url())
|
||||||
|
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
|
||||||
// Wait for login to complete
|
break
|
||||||
await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkBingLogin(page: Page): Promise<void> {
|
|
||||||
try {
|
|
||||||
log('LOGIN-BING', 'Verifying Bing login')
|
|
||||||
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F')
|
|
||||||
|
|
||||||
const maxIterations = 5
|
|
||||||
|
|
||||||
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
||||||
const currentUrl = new URL(page.url())
|
|
||||||
|
|
||||||
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
|
|
||||||
await wait(3000)
|
|
||||||
await tryDismissBingCookieBanner(page)
|
|
||||||
|
|
||||||
const loggedIn = await checkBingLoginStatus(page)
|
|
||||||
if (loggedIn) {
|
|
||||||
log('LOGIN-BING', 'Bing login verification passed!')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await wait(1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
// Wait for login to complete
|
||||||
log('LOGIN-BING', 'An error occurred:' + error, 'error')
|
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 })
|
||||||
|
this.bot.log('LOGIN', 'Successfully logged into the rewards portal')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function checkBingLoginStatus(page: Page): Promise<boolean> {
|
private async checkBingLogin(page: Page): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await page.waitForSelector('#id_n', { timeout: 5000 })
|
this.bot.log('LOGIN-BING', 'Verifying Bing login')
|
||||||
return true
|
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')
|
||||||
} catch (error) {
|
|
||||||
return false
|
const maxIterations = 5
|
||||||
|
|
||||||
|
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
||||||
|
const currentUrl = new URL(page.url())
|
||||||
|
|
||||||
|
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
|
||||||
|
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
|
||||||
|
|
||||||
|
const loggedIn = await this.checkBingLoginStatus(page)
|
||||||
|
// If mobile browser, skip this step
|
||||||
|
if (loggedIn || this.bot.isMobile) {
|
||||||
|
this.bot.log('LOGIN-BING', 'Bing login verification passed!')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('LOGIN-BING', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkBingLoginStatus(page: Page): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('#id_n', { timeout: 5000 })
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMobileAccessToken(page: Page, email: string) {
|
||||||
|
const authorizeUrl = new URL(this.authBaseUrl)
|
||||||
|
|
||||||
|
authorizeUrl.searchParams.append('response_type', 'code')
|
||||||
|
authorizeUrl.searchParams.append('client_id', this.clientId)
|
||||||
|
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
|
||||||
|
authorizeUrl.searchParams.append('scope', this.scope)
|
||||||
|
authorizeUrl.searchParams.append('state', crypto.randomBytes(16).toString('hex'))
|
||||||
|
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||||
|
authorizeUrl.searchParams.append('login_hint', email)
|
||||||
|
|
||||||
|
await page.goto(authorizeUrl.href)
|
||||||
|
|
||||||
|
const currentUrl = new URL(page.url())
|
||||||
|
let code: string
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
this.bot.log('LOGIN-APP', 'Waiting for authorization')
|
||||||
|
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
|
||||||
|
code = currentUrl.searchParams.get('code')!
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
body.append('grant_type', 'authorization_code')
|
||||||
|
body.append('client_id', this.clientId)
|
||||||
|
body.append('code', code)
|
||||||
|
body.append('redirect_uri', this.redirectUrl)
|
||||||
|
|
||||||
|
const tokenRequest = {
|
||||||
|
url: this.tokenUrl,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
data: body.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResponse = await axios(tokenRequest)
|
||||||
|
const tokenData: OAuth = await tokenResponse.data
|
||||||
|
|
||||||
|
this.bot.log('LOGIN-APP', 'Successfully authorized')
|
||||||
|
return tokenData.access_token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,173 +1,211 @@
|
|||||||
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'
|
||||||
|
|
||||||
// Daily Set
|
export class Workers {
|
||||||
export async function doDailySet(page: Page, data: DashboardData) {
|
public bot: MicrosoftRewardsBot
|
||||||
|
|
||||||
const todayData = data.dailySetPromotions[getFormattedDate()]
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
|
this.bot = bot
|
||||||
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
|
|
||||||
|
|
||||||
if (!activitiesUncompleted.length) {
|
|
||||||
log('DAILY-SET', 'All Daily Set" items have already been completed')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Solve Activities
|
// Daily Set
|
||||||
log('DAILY-SET', 'Started solving "Daily Set" items')
|
async doDailySet(page: Page, data: DashboardData) {
|
||||||
|
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
|
||||||
|
|
||||||
await solveActivities(page, activitiesUncompleted)
|
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
|
||||||
|
|
||||||
log('DAILY-SET', 'All "Daily Set" items have been completed')
|
if (!activitiesUncompleted.length) {
|
||||||
}
|
this.bot.log('DAILY-SET', 'All Daily Set" items have already been completed')
|
||||||
|
return
|
||||||
// Punch Card
|
}
|
||||||
export async function doPunchCard(page: Page, data: DashboardData) {
|
|
||||||
|
|
||||||
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
|
|
||||||
|
|
||||||
if (!punchCardsUncompleted.length) {
|
|
||||||
log('PUNCH-CARD', 'All "Punch Cards" have already been completed')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const punchCard of punchCardsUncompleted) {
|
|
||||||
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
|
|
||||||
|
|
||||||
// Solve Activities
|
// Solve Activities
|
||||||
log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
|
this.bot.log('DAILY-SET', 'Started solving "Daily Set" items')
|
||||||
|
|
||||||
const browser = page.browser()
|
await this.solveActivities(page, activitiesUncompleted)
|
||||||
page = await browser.newPage()
|
|
||||||
|
|
||||||
// Got to punch card index page in a new tab
|
page = await this.bot.browser.utils.getLatestTab(page)
|
||||||
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: baseURL })
|
|
||||||
|
|
||||||
await solveActivities(page, activitiesUncompleted, punchCard)
|
// Always return to the homepage if not already
|
||||||
|
await this.bot.browser.func.goHome(page)
|
||||||
|
|
||||||
// Close the punch card index page
|
this.bot.log('DAILY-SET', 'All "Daily Set" items have been completed')
|
||||||
await page.close()
|
|
||||||
|
|
||||||
log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log('PUNCH-CARD', 'All "Punch Card" items have been completed')
|
// Punch Card
|
||||||
}
|
async doPunchCard(page: Page, data: DashboardData) {
|
||||||
|
|
||||||
// More Promotions
|
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion?.complete) ?? [] // Only return uncompleted punch cards
|
||||||
export async function doMorePromotions(page: Page, data: DashboardData) {
|
|
||||||
const morePromotions = data.morePromotions
|
|
||||||
|
|
||||||
// Check if there is a promotional item
|
if (!punchCardsUncompleted.length) {
|
||||||
if (data.promotionalItem) { // Convert and add the promotional item to the array
|
this.bot.log('PUNCH-CARD', 'All "Punch Cards" have already been completed')
|
||||||
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"`)
|
||||||
|
|
||||||
|
// 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: 5000 }).catch(() => { })
|
||||||
|
|
||||||
|
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()
|
||||||
|
} else {
|
||||||
|
await this.bot.browser.func.goHome(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.log('PUNCH-CARD', 'All "Punch Card" items have been completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
|
// More Promotions
|
||||||
|
async doMorePromotions(page: Page, data: DashboardData) {
|
||||||
|
const morePromotions = data.morePromotions
|
||||||
|
|
||||||
if (!activitiesUncompleted.length) {
|
// Check if there is a promotional item
|
||||||
log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
|
if (data.promotionalItem) { // Convert and add the promotional item to the array
|
||||||
return
|
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
|
||||||
|
}
|
||||||
|
|
||||||
|
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && !x.attributes.is_unlocked) ?? []
|
||||||
|
|
||||||
|
if (!activitiesUncompleted.length) {
|
||||||
|
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solve Activities
|
||||||
|
this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" items')
|
||||||
|
|
||||||
|
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 Activities
|
// Solve all the different types of activities
|
||||||
log('MORE-PROMOTIONS', 'Started solving "More Promotions" item')
|
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
|
||||||
|
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
|
||||||
|
|
||||||
await solveActivities(page, activitiesUncompleted)
|
for (const activity of activities) {
|
||||||
|
try {
|
||||||
|
// Reselect the worker page
|
||||||
|
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||||
|
|
||||||
log('MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
|
const pages = activityPage.context().pages()
|
||||||
}
|
if (pages.length > 3) {
|
||||||
|
await activityPage.close()
|
||||||
|
|
||||||
// Solve all the different types of activities
|
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
|
||||||
async function solveActivities(page: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
|
|
||||||
for (const activity of activities) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
let selector = `[data-bi-id="${activity.offerId}"]`
|
|
||||||
|
|
||||||
if (punchCard) {
|
|
||||||
selector = await getPunchCardActivity(page, activity)
|
|
||||||
|
|
||||||
} else if (activity.name.toLowerCase().includes('membercenter')) {
|
|
||||||
|
|
||||||
// Promotion
|
|
||||||
if (activity.priority === 1) {
|
|
||||||
selector = '#promo-item'
|
|
||||||
} else {
|
|
||||||
selector = `[data-bi-id="${activity.name}"]`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(activityPage, activity)
|
||||||
|
|
||||||
|
} else if (activity.name.toLowerCase().includes('membercenter')) {
|
||||||
|
selector = `[data-bi-id^="${activity.name}"]`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click element, it will be opened in a new tab
|
||||||
|
await activityPage.click(selector)
|
||||||
|
|
||||||
|
// Select the new activity 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) {
|
||||||
|
// Quiz (Poll, Quiz or ABC)
|
||||||
|
case 'quiz':
|
||||||
|
switch (activity.pointProgressMax) {
|
||||||
|
// Poll or ABC (Usually 10 points)
|
||||||
|
case 10:
|
||||||
|
// Normal poll
|
||||||
|
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doPoll(activityPage)
|
||||||
|
} else { // ABC
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doABC(activityPage)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
// This Or That Quiz (Usually 50 points)
|
||||||
|
case 50:
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doThisOrThat(activityPage)
|
||||||
|
break
|
||||||
|
|
||||||
|
// Quizzes are usually 30-40 points
|
||||||
|
default:
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doQuiz(activityPage)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
// UrlReward (Visit)
|
||||||
|
case 'urlreward':
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doUrlReward(activityPage)
|
||||||
|
break
|
||||||
|
|
||||||
|
// Misc, Usually UrlReward Type
|
||||||
|
default:
|
||||||
|
this.bot.log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`)
|
||||||
|
await this.bot.activities.doUrlReward(activityPage)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooldown
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for element to load
|
|
||||||
await page.waitForSelector(selector, { timeout: 5000 })
|
|
||||||
// Click element, it will be opened in a new tab
|
|
||||||
await page.click(selector)
|
|
||||||
|
|
||||||
// Select the new activity page
|
|
||||||
const activityPage = await getLatestTab(page)
|
|
||||||
|
|
||||||
switch (activity.promotionType) {
|
|
||||||
// Quiz (Poll, Quiz or ABC)
|
|
||||||
case 'quiz':
|
|
||||||
switch (activity.pointProgressMax) {
|
|
||||||
// Poll or ABC (Usually 10 points)
|
|
||||||
case 10:
|
|
||||||
// Normal poll
|
|
||||||
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
|
|
||||||
log('ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
|
|
||||||
await doPoll(activityPage)
|
|
||||||
} else { // ABC
|
|
||||||
log('ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
|
|
||||||
await doABC(activityPage)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
// This Or That Quiz (Usually 50 points)
|
|
||||||
case 50:
|
|
||||||
log('ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
|
|
||||||
await doThisOrThat(activityPage)
|
|
||||||
break
|
|
||||||
|
|
||||||
// Quizzes are usually 30-40 points
|
|
||||||
default:
|
|
||||||
log('ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
|
|
||||||
await doQuiz(activityPage)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
// UrlReward (Visit)
|
|
||||||
case 'urlreward':
|
|
||||||
log('ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
|
|
||||||
await doUrlReward(activityPage)
|
|
||||||
break
|
|
||||||
|
|
||||||
// Misc, Usually UrlReward Type
|
|
||||||
default:
|
|
||||||
log('ACTIVITY', `Found activity type: "Misc" title: "${activity.title}"`)
|
|
||||||
await doUrlReward(activityPage)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cooldown
|
|
||||||
await wait(1500)
|
|
||||||
} catch (error) {
|
|
||||||
log('ACTIVITY', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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')
|
|
||||||
|
|
||||||
try {
|
export class ABC extends Workers {
|
||||||
await wait(2000)
|
|
||||||
let $ = await refreshCheerio(page)
|
|
||||||
|
|
||||||
// Don't loop more than 15 in case unable to solve, would lock otherwise
|
async doABC(page: Page) {
|
||||||
const maxIterations = 15
|
this.bot.log('ABC', 'Trying to complete poll')
|
||||||
let i
|
|
||||||
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
|
|
||||||
await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 5000 })
|
|
||||||
|
|
||||||
const answers = $('.wk_OptionClickClass')
|
try {
|
||||||
const answer = answers[randomNumber(0, 2)]?.attribs['id']
|
let $ = await this.bot.browser.func.refreshCheerio(page)
|
||||||
|
|
||||||
await page.waitForSelector(`#${answer}`, { visible: true, timeout: 5000 })
|
// Don't loop more than 15 in case unable to solve, would lock otherwise
|
||||||
|
const maxIterations = 15
|
||||||
|
let i
|
||||||
|
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
|
||||||
|
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 })
|
||||||
|
|
||||||
await wait(2000)
|
const answers = $('.wk_OptionClickClass')
|
||||||
await page.click(`#${answer}`) // Click answer
|
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
|
||||||
|
|
||||||
await wait(4000)
|
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 })
|
||||||
await page.waitForSelector('div.wk_button', { visible: true, timeout: 5000 })
|
|
||||||
await page.click('div.wk_button') // Click next question button
|
|
||||||
|
|
||||||
page = await getLatestTab(page)
|
await this.bot.utils.wait(2000)
|
||||||
$ = await refreshCheerio(page)
|
await page.click(`#${answer}`) // Click answer
|
||||||
await wait(1000)
|
|
||||||
|
await this.bot.utils.wait(4000)
|
||||||
|
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)
|
||||||
|
$ = await this.bot.browser.func.refreshCheerio(page)
|
||||||
|
await this.bot.utils.wait(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(4000)
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
if (i === maxIterations) {
|
||||||
|
this.bot.log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
|
||||||
|
} else {
|
||||||
|
this.bot.log('ABC', 'Completed the ABC successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('ABC', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
|
|
||||||
await wait(4000)
|
|
||||||
await page.close()
|
|
||||||
|
|
||||||
if (i === maxIterations) {
|
|
||||||
log('ABC', 'Failed to solve quiz, exceeded max iterations of 15', 'warn')
|
|
||||||
} else {
|
|
||||||
log('ABC', 'Completed the ABC successfully')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await page.close()
|
|
||||||
log('ABC', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
48
src/functions/activities/DailyCheckIn.ts
Normal file
48
src/functions/activities/DailyCheckIn.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
import { Workers } from '../Workers'
|
||||||
|
|
||||||
|
import { DashboardData } from '../../interface/DashboardData'
|
||||||
|
|
||||||
|
|
||||||
|
export class DailyCheckIn extends Workers {
|
||||||
|
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
|
||||||
|
this.bot.log('DAILY-CHECK-IN', 'Starting Daily Check In')
|
||||||
|
|
||||||
|
try {
|
||||||
|
let geoLocale = data.userProfile.attributes.country
|
||||||
|
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||||
|
|
||||||
|
const jsonData = {
|
||||||
|
amount: 1,
|
||||||
|
country: geoLocale,
|
||||||
|
id: randomBytes(64).toString('hex'),
|
||||||
|
type: 101,
|
||||||
|
attributes: {
|
||||||
|
offerid: 'Gamification_Sapphire_DailyCheckIn'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimRequest = {
|
||||||
|
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Rewards-Country': geoLocale,
|
||||||
|
'X-Rewards-Language': 'en'
|
||||||
|
},
|
||||||
|
data: JSON.stringify(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimResponse = await axios(claimRequest)
|
||||||
|
const claimedPoint = parseInt((await claimResponse.data).response.activity.p)
|
||||||
|
|
||||||
|
this.bot.log('DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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')
|
|
||||||
|
|
||||||
try {
|
export class Poll extends Workers {
|
||||||
const buttonId = `#btoption${Math.floor(randomNumber(0, 1))}`
|
|
||||||
|
|
||||||
await page.waitForNetworkIdle({ timeout: 5000 })
|
async doPoll(page: Page) {
|
||||||
await page.waitForSelector(buttonId, { visible: true, timeout: 5000 })
|
this.bot.log('POLL', 'Trying to complete poll')
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
await page.click(buttonId)
|
try {
|
||||||
|
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
|
||||||
|
|
||||||
await wait(4000)
|
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not
|
||||||
await page.close()
|
await this.bot.utils.wait(2000)
|
||||||
|
|
||||||
log('POLL', 'Completed the poll successfully')
|
await page.click(buttonId)
|
||||||
} catch (error) {
|
|
||||||
await page.close()
|
await this.bot.utils.wait(4000)
|
||||||
log('POLL', 'An error occurred:' + error, 'error')
|
await page.close()
|
||||||
|
|
||||||
|
this.bot.log('POLL', 'Completed the poll successfully')
|
||||||
|
} catch (error) {
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('POLL', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,94 +1,91 @@
|
|||||||
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')
|
|
||||||
|
|
||||||
try {
|
export class Quiz extends Workers {
|
||||||
await page.waitForNetworkIdle({ timeout: 5000 })
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
// Check if the quiz has been started or not
|
async doQuiz(page: Page) {
|
||||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
|
this.bot.log('QUIZ', 'Trying to complete quiz')
|
||||||
if (quizNotStarted) {
|
|
||||||
await page.click('#rqStartQuiz')
|
|
||||||
} else {
|
|
||||||
log('QUIZ', 'Quiz has already been started, trying to finish it')
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(2000)
|
try {
|
||||||
|
// Check if the quiz has been started or not
|
||||||
|
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||||
|
if (quizNotStarted) {
|
||||||
|
await page.click('#rqStartQuiz')
|
||||||
|
} else {
|
||||||
|
this.bot.log('QUIZ', 'Quiz has already been started, trying to finish it')
|
||||||
|
}
|
||||||
|
|
||||||
let quizData = await getQuizData(page)
|
await this.bot.utils.wait(2000)
|
||||||
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
|
|
||||||
|
|
||||||
// All questions
|
let quizData = await this.bot.browser.func.getQuizData(page)
|
||||||
for (let question = 0; question < questionsRemaining; question++) {
|
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
|
||||||
|
|
||||||
if (quizData.numberOfOptions === 8) {
|
// All questions
|
||||||
const answers: string[] = []
|
for (let question = 0; question < questionsRemaining; question++) {
|
||||||
|
|
||||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
if (quizData.numberOfOptions === 8) {
|
||||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 })
|
const answers: string[] = []
|
||||||
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
|
|
||||||
|
|
||||||
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
|
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
||||||
answers.push(`#rqAnswerOption${i}`)
|
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') {
|
||||||
|
answers.push(`#rqAnswerOption${i}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
await page.click(answer)
|
|
||||||
|
|
||||||
const refreshSuccess = await waitForQuizRefresh(page)
|
|
||||||
if (!refreshSuccess) {
|
|
||||||
await page.close()
|
|
||||||
log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other type quiz
|
|
||||||
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
|
|
||||||
quizData = await getQuizData(page) // Refresh Quiz Data
|
|
||||||
const correctOption = quizData.correctAnswer
|
|
||||||
|
|
||||||
for (let i = 0; i < quizData.numberOfOptions; i++) {
|
|
||||||
|
|
||||||
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 5000 })
|
|
||||||
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
|
|
||||||
|
|
||||||
if (dataOption === correctOption) {
|
|
||||||
// Click the answer on page
|
// Click the answer on page
|
||||||
await page.click(`#rqAnswerOption${i}`)
|
await page.click(answer)
|
||||||
|
|
||||||
const refreshSuccess = await waitForQuizRefresh(page)
|
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
|
||||||
if (!refreshSuccess) {
|
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)
|
|
||||||
|
|
||||||
|
// 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}`, { state: 'visible', timeout: 10_000 })
|
||||||
|
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
|
||||||
|
|
||||||
|
if (dataOption === correctOption) {
|
||||||
|
// Click the answer on page
|
||||||
|
await page.click(`#rqAnswerOption${i}`)
|
||||||
|
|
||||||
|
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
|
||||||
|
if (!refreshSuccess) {
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Done with
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('QUIZ', 'Completed the quiz successfully')
|
||||||
|
} catch (error) {
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('QUIZ', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done with
|
|
||||||
await wait(2000)
|
|
||||||
await page.close()
|
|
||||||
log('QUIZ', 'Completed the quiz successfully')
|
|
||||||
} catch (error) {
|
|
||||||
await page.close()
|
|
||||||
log('QUIZ', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
73
src/functions/activities/ReadToEarn.ts
Normal file
73
src/functions/activities/ReadToEarn.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
import { Workers } from '../Workers'
|
||||||
|
|
||||||
|
import { DashboardData } from '../../interface/DashboardData'
|
||||||
|
|
||||||
|
|
||||||
|
export class ReadToEarn extends Workers {
|
||||||
|
public async doReadToEarn(accessToken: string, data: DashboardData) {
|
||||||
|
this.bot.log('READ-TO-EARN', 'Starting Read to Earn')
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
let geoLocale = data.userProfile.attributes.country
|
||||||
|
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
|
||||||
|
|
||||||
|
const userDataRequest = {
|
||||||
|
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me',
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'X-Rewards-Country': geoLocale,
|
||||||
|
'X-Rewards-Language': 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userDataResponse = await axios(userDataRequest)
|
||||||
|
const userData = (await userDataResponse.data).response
|
||||||
|
let balance: number = userData.balance
|
||||||
|
|
||||||
|
const jsonData = {
|
||||||
|
amount: 1,
|
||||||
|
country: geoLocale,
|
||||||
|
id: '1',
|
||||||
|
type: 101,
|
||||||
|
attributes: {
|
||||||
|
offerid: 'ENUS_readarticle3_30points'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; ++i) {
|
||||||
|
jsonData.id = randomBytes(64).toString('hex')
|
||||||
|
const claimRequest = {
|
||||||
|
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Rewards-Country': geoLocale,
|
||||||
|
'X-Rewards-Language': 'en'
|
||||||
|
},
|
||||||
|
data: JSON.stringify(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimResponse = await axios(claimRequest)
|
||||||
|
const newBalance = (await claimResponse.data).response.balance
|
||||||
|
|
||||||
|
if (newBalance == balance) {
|
||||||
|
this.bot.log('READ-TO-EARN', 'Read all available articles')
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
balance = newBalance
|
||||||
|
this.bot.log('READ-TO-EARN', `Read article ${i + 1}`)
|
||||||
|
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bot.log('READ-TO-EARN', 'Completed Read to Earn')
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('READ-TO-EARN', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,203 +1,251 @@
|
|||||||
import { Page } from 'puppeteer'
|
import { Page } from 'playwright'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { platform } from 'os'
|
||||||
|
|
||||||
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')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate search queries
|
page = await this.bot.browser.utils.getLatestTab(page)
|
||||||
let googleSearchQueries = await getGoogleTrends(data.userProfile.attributes.country, missingPoints)
|
|
||||||
googleSearchQueries = shuffleArray(googleSearchQueries)
|
|
||||||
|
|
||||||
// Deduplicate the search terms
|
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
|
||||||
googleSearchQueries = [...new Set(googleSearchQueries)]
|
let missingPoints = this.calculatePoints(searchCounters)
|
||||||
|
|
||||||
// Open a new tab
|
if (missingPoints === 0) {
|
||||||
const browser = page.browser()
|
this.bot.log('SEARCH-BING', `Bing searches for ${this.bot.isMobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
|
||||||
const searchPage = await browser.newPage()
|
return
|
||||||
|
|
||||||
// Go to bing
|
|
||||||
await searchPage.goto('https://bing.com')
|
|
||||||
|
|
||||||
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search
|
|
||||||
|
|
||||||
const queries: string[] = []
|
|
||||||
googleSearchQueries.forEach(x => queries.push(x.topic, ...x.related))
|
|
||||||
|
|
||||||
// Loop over Google search queries
|
|
||||||
for (let i = 0; i < queries.length; i++) {
|
|
||||||
const query = queries[i] as string
|
|
||||||
|
|
||||||
log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${mobile}`)
|
|
||||||
|
|
||||||
const newData = await bingSearch(page, searchPage, query)
|
|
||||||
|
|
||||||
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches
|
|
||||||
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches
|
|
||||||
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
|
|
||||||
|
|
||||||
const newMissingPoints = (mobile && newMobileData) ?
|
|
||||||
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
|
|
||||||
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
|
|
||||||
|
|
||||||
// If the new point amount is the same as before
|
|
||||||
if (newMissingPoints == missingPoints) {
|
|
||||||
maxLoop++ // Add to max loop
|
|
||||||
} else { // There has been a change in points
|
|
||||||
maxLoop = 0 // Reset the loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
missingPoints = newMissingPoints
|
// Generate search queries
|
||||||
|
let googleSearchQueries = await this.getGoogleTrends(data.userProfile.attributes.country, missingPoints)
|
||||||
|
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
|
||||||
|
|
||||||
if (missingPoints == 0) {
|
// Deduplicate the search terms
|
||||||
break
|
googleSearchQueries = [...new Set(googleSearchQueries)]
|
||||||
|
|
||||||
|
// Go to bing
|
||||||
|
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[] = []
|
||||||
|
// 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) })
|
||||||
|
|
||||||
|
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
|
||||||
|
|
||||||
|
// 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: ${this.bot.isMobile}`)
|
||||||
|
|
||||||
|
searchCounters = await this.bingSearch(page, query)
|
||||||
|
const newMissingPoints = this.calculatePoints(searchCounters)
|
||||||
|
|
||||||
|
// If the new point amount is the same as before
|
||||||
|
if (newMissingPoints == missingPoints) {
|
||||||
|
maxLoop++ // Add to max loop
|
||||||
|
} else { // There has been a change in points
|
||||||
|
maxLoop = 0 // Reset the loop
|
||||||
|
}
|
||||||
|
|
||||||
|
missingPoints = newMissingPoints
|
||||||
|
|
||||||
|
if (missingPoints === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only for mobile searches
|
||||||
|
if (maxLoop > 3 && this.bot.isMobile) {
|
||||||
|
this.bot.log('SEARCH-BING-MOBILE', 'Search didn\'t gain point for 3 iterations, likely bad User-Agent', 'warn')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't gain points for 10 iterations, assume it's stuck
|
||||||
|
if (maxLoop > 10) {
|
||||||
|
this.bot.log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
|
||||||
|
maxLoop = 0 // Reset to 0 so we can retry with related searches below
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't gain points for 10 iterations, assume it's stuck
|
// Only for mobile searches
|
||||||
if (maxLoop > 10) {
|
if (missingPoints > 0 && this.bot.isMobile) {
|
||||||
log('SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
|
return
|
||||||
maxLoop = 0 // Reset to 0 so we can retry with related searches below
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If we still got remaining search queries, generate extra ones
|
// If 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, term)
|
||||||
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) ?
|
// If the new point amount is the same as before
|
||||||
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
|
if (newMissingPoints == missingPoints) {
|
||||||
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
|
maxLoop++ // Add to max loop
|
||||||
|
} else { // There has been a change in points
|
||||||
|
maxLoop = 0 // Reset the loop
|
||||||
|
}
|
||||||
|
|
||||||
// If the new point amount is the same as before
|
missingPoints = newMissingPoints
|
||||||
if (newMissingPoints == missingPoints) {
|
|
||||||
maxLoop++ // Add to max loop
|
|
||||||
} else { // There has been a change in points
|
|
||||||
maxLoop = 0 // Reset the loop
|
|
||||||
}
|
|
||||||
|
|
||||||
missingPoints = newMissingPoints
|
// If we satisfied the searches
|
||||||
|
if (missingPoints === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// If we satisfied the searches
|
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
|
||||||
if (missingPoints == 0) {
|
if (maxLoop > 5) {
|
||||||
break
|
this.bot.log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
|
||||||
}
|
return
|
||||||
|
}
|
||||||
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
|
|
||||||
if (maxLoop > 5) {
|
|
||||||
log('SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bot.log('SEARCH-BING', 'Completed searches')
|
||||||
}
|
}
|
||||||
|
|
||||||
log('SEARCH-BING', 'Completed searches')
|
private async bingSearch(searchPage: Page, query: string) {
|
||||||
}
|
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
|
||||||
|
|
||||||
async function bingSearch(page: Page, searchPage: Page, query: string) {
|
|
||||||
// Try a max of 5 times
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
try {
|
|
||||||
const searchBar = '#sb_form_q'
|
|
||||||
await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 })
|
|
||||||
await searchPage.click(searchBar) // Focus on the textarea
|
|
||||||
await wait(500)
|
|
||||||
await searchPage.keyboard.down('Control')
|
|
||||||
await searchPage.keyboard.press('A')
|
|
||||||
await searchPage.keyboard.press('Backspace')
|
|
||||||
await searchPage.keyboard.up('Control')
|
|
||||||
await searchPage.keyboard.type(query)
|
|
||||||
await searchPage.keyboard.press('Enter')
|
|
||||||
|
|
||||||
if (searchSettings.scrollRandomResults) {
|
|
||||||
await wait(2000)
|
// Try a max of 5 times
|
||||||
await randomScroll(searchPage)
|
for (let i = 0; i < 5; i++) {
|
||||||
|
try {
|
||||||
|
// Go to top of the page
|
||||||
|
await searchPage.evaluate(() => {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set it since params get added after visiting
|
||||||
|
this.searchPageURL = searchPage.url()
|
||||||
|
|
||||||
|
await this.bot.utils.wait(500)
|
||||||
|
|
||||||
|
const searchBar = '#sb_form_q'
|
||||||
|
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(platformControlKey)
|
||||||
|
await searchPage.keyboard.press('A')
|
||||||
|
await searchPage.keyboard.press('Backspace')
|
||||||
|
await searchPage.keyboard.up(platformControlKey)
|
||||||
|
await searchPage.keyboard.type(query)
|
||||||
|
await searchPage.keyboard.press('Enter')
|
||||||
|
|
||||||
|
await this.bot.utils.wait(1000)
|
||||||
|
|
||||||
|
// Bing.com in Chrome opens a new tab when searching
|
||||||
|
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||||
|
|
||||||
|
if (this.bot.config.searchSettings.scrollRandomResults) {
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
await this.randomScroll(resultPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bot.config.searchSettings.clickRandomResults) {
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
await this.clickRandomLink(resultPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay between searches
|
||||||
|
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()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (i === 5) {
|
||||||
|
this.bot.log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
|
||||||
|
break
|
||||||
|
|
||||||
|
}
|
||||||
|
this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
|
||||||
|
this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
|
||||||
|
|
||||||
|
// Reset the tabs
|
||||||
|
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
|
||||||
|
await this.closeTabs(lastTab)
|
||||||
|
|
||||||
|
await this.bot.utils.wait(4000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchSettings.clickRandomResults) {
|
|
||||||
await wait(2000)
|
|
||||||
await clickRandomLink(searchPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(Math.floor(randomNumber(10_000, 20_000)))
|
|
||||||
|
|
||||||
return await getSearchPoints(page)
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (i === 5) {
|
|
||||||
log('SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
|
|
||||||
break
|
|
||||||
|
|
||||||
}
|
|
||||||
log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
|
|
||||||
log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
|
|
||||||
|
|
||||||
await wait(4000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
|
||||||
|
return await this.bot.browser.func.getSearchPoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
|
private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> {
|
||||||
return await getSearchPoints(page)
|
const queryTerms: GoogleSearch[] = []
|
||||||
}
|
let i = 0
|
||||||
|
|
||||||
async function getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> {
|
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US'
|
||||||
const queryTerms: GoogleSearch[] = []
|
|
||||||
let i = 0
|
|
||||||
|
|
||||||
geoLocale = (searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toUpperCase() : 'US'
|
this.bot.log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
|
||||||
|
|
||||||
log('SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
|
while (queryCount > queryTerms.length) {
|
||||||
|
i += 1
|
||||||
|
const date = new Date()
|
||||||
|
date.setDate(date.getDate() - i)
|
||||||
|
const formattedDate = this.formatDate(date)
|
||||||
|
|
||||||
while (queryCount > queryTerms.length) {
|
try {
|
||||||
i += 1
|
const request = {
|
||||||
const date = new Date()
|
url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`,
|
||||||
date.setDate(date.getDate() - i)
|
method: 'GET',
|
||||||
const formattedDate = formatDate(date)
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios(request)
|
||||||
|
|
||||||
|
const data: GoogleTrends = JSON.parse((await response.data).slice(5))
|
||||||
|
|
||||||
|
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
|
||||||
|
queryTerms.push({
|
||||||
|
topic: topic.title.query.toLowerCase(),
|
||||||
|
related: topic.relatedQueries.map(x => x.query.toLowerCase())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryTerms
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRelatedTerms(term: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const request = {
|
const request = {
|
||||||
url: `https://trends.google.com/trends/api/dailytrends?geo=${geoLocale}&hl=en&ed=${formattedDate}&ns=15`,
|
url: `https://api.bing.com/osjson.aspx?query=${term}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -206,126 +254,95 @@ async function getGoogleTrends(geoLocale: string, queryCount: number): Promise<G
|
|||||||
|
|
||||||
const response = await axios(request)
|
const response = await axios(request)
|
||||||
|
|
||||||
const data: GoogleTrends = JSON.parse((await response.data).slice(5))
|
return response.data[1] as string[]
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
|
private formatDate(date: Date): string {
|
||||||
queryTerms.push({
|
const year = date.getFullYear()
|
||||||
topic: topic.title.query.toLowerCase(),
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
related: topic.relatedQueries.map(x => x.query.toLocaleLowerCase())
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
})
|
|
||||||
}
|
return `${year}${month}${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private async randomScroll(page: Page) {
|
||||||
|
try {
|
||||||
|
const viewportHeight = await page.evaluate(() => window.innerHeight)
|
||||||
|
const totalHeight = await page.evaluate(() => document.body.scrollHeight)
|
||||||
|
const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
|
||||||
|
|
||||||
|
await page.evaluate((scrollPos) => {
|
||||||
|
window.scrollTo(0, scrollPos)
|
||||||
|
}, randomScrollPosition)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
|
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryTerms
|
private async clickRandomLink(page: Page) {
|
||||||
}
|
try {
|
||||||
|
|
||||||
async function getRelatedTerms(term: string): Promise<string[]> {
|
await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
|
||||||
try {
|
|
||||||
const request = {
|
|
||||||
url: `https://api.bing.com/osjson.aspx?query=${term}`,
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios(request)
|
// Will get current tab if no new one is created
|
||||||
|
let lastTab = await this.bot.browser.utils.getLatestTab(page)
|
||||||
|
|
||||||
return response.data[1] as string[]
|
// Stay for 10 seconds
|
||||||
} catch (error) {
|
await this.bot.utils.wait(10_000)
|
||||||
log('SEARCH-BING-RELTATED', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
|
||||||
const year = date.getFullYear()
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(date.getDate()).padStart(2, '0')
|
|
||||||
|
|
||||||
return `${year}${month}${day}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function randomScroll(page: Page) {
|
|
||||||
try {
|
|
||||||
// Press the arrow down key to scroll
|
|
||||||
for (let i = 0; i < randomNumber(5, 100); i++) {
|
|
||||||
await page.keyboard.press('ArrowDown')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickRandomLink(page: Page) {
|
|
||||||
try {
|
|
||||||
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
|
|
||||||
|
|
||||||
// Wait for website to load
|
|
||||||
await wait(3000)
|
|
||||||
|
|
||||||
// Will get current tab if no new one is created
|
|
||||||
let lastTab = await getLatestTab(page)
|
|
||||||
|
|
||||||
// Wait for the body of the new page to be loaded
|
|
||||||
await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { })
|
|
||||||
|
|
||||||
// Check if the tab is closed or not
|
|
||||||
if (!lastTab.isClosed()) {
|
|
||||||
let lastTabURL = new URL(lastTab.url()) // Get new tab info
|
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 !== this.searchPageURL && 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)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
i++
|
||||||
|
|
||||||
} 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
|
} catch (error) {
|
||||||
i++
|
this.bot.log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async closeTabs(lastTab: Page) {
|
||||||
|
const browser = lastTab.context()
|
||||||
|
const tabs = browser.pages()
|
||||||
|
|
||||||
|
// If more than 2 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(this.searchPageURL)
|
||||||
|
|
||||||
|
// Else go back one page, this means the correct amount is open
|
||||||
|
} else {
|
||||||
|
await lastTab.goBack().catch(() => { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,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 async function doThisOrThat(page: Page) {
|
|
||||||
log('THIS-OR-THAT', 'Trying to complete ThisOrThat')
|
|
||||||
|
|
||||||
try {
|
export class ThisOrThat extends Workers {
|
||||||
await page.waitForNetworkIdle({ timeout: 5000 })
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
// Check if the quiz has been started or not
|
async doThisOrThat(page: Page) {
|
||||||
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
|
this.bot.log('THIS-OR-THAT', 'Trying to complete ThisOrThat')
|
||||||
if (quizNotStarted) {
|
|
||||||
await page.click('#rqStartQuiz')
|
|
||||||
} else {
|
try {
|
||||||
log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
|
// Check if the quiz has been started or not
|
||||||
|
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
|
||||||
|
if (quizNotStarted) {
|
||||||
|
await page.click('#rqStartQuiz')
|
||||||
|
} else {
|
||||||
|
this.bot.log('THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.bot.utils.wait(2000)
|
||||||
|
|
||||||
|
// Solving
|
||||||
|
const quizData = await this.bot.browser.func.getQuizData(page)
|
||||||
|
const questionsRemaining = quizData.maxQuestions - (quizData.currentQuestionNumber - 1) // Amount of questions remaining
|
||||||
|
|
||||||
|
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
// Solving
|
|
||||||
const quizData = await getQuizData(page)
|
|
||||||
quizData // correctAnswer property is always null?
|
|
||||||
|
|
||||||
log('THIS-OR-THAT', 'Completed the ThisOrthat successfully')
|
|
||||||
} catch (error) {
|
|
||||||
await page.close()
|
|
||||||
log('THIS-OR-THAT', 'An error occurred:' + error, 'error')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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')
|
|
||||||
|
|
||||||
try {
|
export class UrlReward extends Workers {
|
||||||
// After waiting, close the page
|
|
||||||
await page.waitForNetworkIdle({ timeout: 10_000 })
|
|
||||||
await wait(2000)
|
|
||||||
await page.close()
|
|
||||||
|
|
||||||
log('URL-REWARD', 'Completed the UrlReward successfully')
|
async doUrlReward(page: Page) {
|
||||||
} catch (error) {
|
this.bot.log('URL-REWARD', 'Trying to complete UrlReward')
|
||||||
await page.close()
|
|
||||||
log('URL-REWARD', 'An error occurred:' + error, 'error')
|
try {
|
||||||
|
this.bot.utils.wait(2000)
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
this.bot.log('URL-REWARD', 'Completed the UrlReward successfully')
|
||||||
|
} catch (error) {
|
||||||
|
await page.close()
|
||||||
|
this.bot.log('URL-REWARD', 'An error occurred:' + error, 'error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
205
src/index.ts
205
src/index.ts
@@ -1,40 +1,64 @@
|
|||||||
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)
|
||||||
|
private accessToken: string = ''
|
||||||
|
|
||||||
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 +72,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 +95,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 +109,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 +126,150 @@ class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
// Desktop
|
// Desktop
|
||||||
async Desktop(account: Account) {
|
async Desktop(account: Account) {
|
||||||
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, false)
|
this.isMobile = 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
|
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 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)
|
||||||
|
this.accessToken = await this.login.getMobileAccessToken(this.homePage, account.email)
|
||||||
|
|
||||||
const wentHome = await goHome(page)
|
await this.browser.func.goHome(this.homePage)
|
||||||
if (!wentHome) {
|
|
||||||
throw log('MAIN', 'Unable to get dashboard page', 'error')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getDashboardData(page)
|
const data = await this.browser.func.getDashboardData()
|
||||||
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
|
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
|
||||||
|
|
||||||
const earnablePoints = await getEarnablePoints(data)
|
const browserEnarablePoints = await this.browser.func.getBrowserEarnablePoints()
|
||||||
|
const appEarnablePoints = await this.browser.func.getAppEarnablePoints(this.accessToken)
|
||||||
|
|
||||||
|
const earnablePoints = browserEnarablePoints + appEarnablePoints
|
||||||
this.collectedPoints = earnablePoints
|
this.collectedPoints = earnablePoints
|
||||||
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`)
|
log('MAIN-POINTS', `You can earn ${earnablePoints} points today (Browser: ${browserEnarablePoints} points, App: ${appEarnablePoints} points)`)
|
||||||
|
|
||||||
// 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
|
||||||
|
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
|
||||||
|
|
||||||
// Close desktop browser
|
// Close desktop browser
|
||||||
await browser.close()
|
await this.closeBrowser(browser, account.email)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
// Do daily check in
|
||||||
|
if (this.config.workers.doDailyCheckIn) {
|
||||||
// If no mobile searches data found, stop (Does not exist on new accounts)
|
await this.activities.doDailyCheckIn(this.accessToken, data)
|
||||||
if (!data.userStatus.counters.mobileSearch) {
|
|
||||||
log('MAIN', 'No mobile searches found, stopping')
|
|
||||||
|
|
||||||
// Close mobile browser
|
|
||||||
return await browser.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do mobile searches
|
// Do read to earn
|
||||||
if (workers.doMobileSearch) {
|
if (this.config.workers.doReadToEarn) {
|
||||||
await doSearch(page, data, true)
|
await this.activities.doReadToEarn(this.accessToken, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no mobile searches data found, stop (Does not exist on new accounts)
|
||||||
|
if (data.userStatus.counters.mobileSearch) {
|
||||||
|
// 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(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('MAIN', 'No mobile searches found!')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch new points
|
// Fetch new points
|
||||||
const earnablePoints = await getEarnablePoints(data, page)
|
const earnablePoints = await this.browser.func.getBrowserEarnablePoints() + await this.browser.func.getAppEarnablePoints(this.accessToken)
|
||||||
|
|
||||||
// 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
|
||||||
|
await this.closeBrowser(browser, account.email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
private async closeBrowser(browser: BrowserContext, email: string) {
|
||||||
|
// Save cookies
|
||||||
|
await saveSessionData(this.config.sessionPath, browser, email, this.isMobile)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
226
src/interface/AppUserData.ts
Normal file
226
src/interface/AppUserData.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
export interface AppUserData {
|
||||||
|
response: Response;
|
||||||
|
correlationId: string;
|
||||||
|
code: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
profile: Profile;
|
||||||
|
balance: number;
|
||||||
|
counters: null;
|
||||||
|
promotions: Promotion[];
|
||||||
|
catalog: null;
|
||||||
|
goal_item: GoalItem;
|
||||||
|
activities: null;
|
||||||
|
cashback: null;
|
||||||
|
orders: Order[];
|
||||||
|
rebateProfile: null;
|
||||||
|
rebatePayouts: null;
|
||||||
|
giveProfile: GiveProfile;
|
||||||
|
autoRedeemProfile: null;
|
||||||
|
autoRedeemItem: null;
|
||||||
|
thirdPartyProfile: null;
|
||||||
|
notifications: null;
|
||||||
|
waitlist: null;
|
||||||
|
autoOpenFlyout: null;
|
||||||
|
coupons: null;
|
||||||
|
recommendedAffordableCatalog: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiveProfile {
|
||||||
|
give_user: string;
|
||||||
|
give_organization: { [key: string]: GiveOrganization | null };
|
||||||
|
first_give_optin: string;
|
||||||
|
last_give_optout: string;
|
||||||
|
give_lifetime_balance: string;
|
||||||
|
give_lifetime_donation_balance: string;
|
||||||
|
give_balance: string;
|
||||||
|
form: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiveOrganization {
|
||||||
|
give_organization_donation_points: number;
|
||||||
|
give_organization_donation_point_to_currency_ratio: number;
|
||||||
|
give_organization_donation_currency: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoalItem {
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
price: number;
|
||||||
|
attributes: GoalItemAttributes;
|
||||||
|
config: GoalItemConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoalItemAttributes {
|
||||||
|
category: string;
|
||||||
|
CategoryDescription: string;
|
||||||
|
'desc.group_text': string;
|
||||||
|
'desc.legal_text'?: string;
|
||||||
|
'desc.sc_description': string;
|
||||||
|
'desc.sc_title': string;
|
||||||
|
display_order: string;
|
||||||
|
ExtraLargeImage: string;
|
||||||
|
group: string;
|
||||||
|
group_image: string;
|
||||||
|
group_sc_image: string;
|
||||||
|
group_title: string;
|
||||||
|
hidden?: string;
|
||||||
|
large_image: string;
|
||||||
|
large_sc_image: string;
|
||||||
|
medium_image: string;
|
||||||
|
MobileImage: string;
|
||||||
|
original_price: string;
|
||||||
|
Remarks?: string;
|
||||||
|
ShortText?: string;
|
||||||
|
showcase?: string;
|
||||||
|
small_image: string;
|
||||||
|
title: string;
|
||||||
|
cimsid: string;
|
||||||
|
user_defined_goal?: string;
|
||||||
|
disable_bot_redemptions?: string;
|
||||||
|
'desc.large_text'?: string;
|
||||||
|
english_title?: string;
|
||||||
|
etid?: string;
|
||||||
|
sku?: string;
|
||||||
|
coupon_discount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoalItemConfig {
|
||||||
|
amount: string;
|
||||||
|
currencyCode: string;
|
||||||
|
isHidden: string;
|
||||||
|
PointToCurrencyConversionRatio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
id: string;
|
||||||
|
t: Date;
|
||||||
|
sku: string;
|
||||||
|
item_snapshot: ItemSnapshot;
|
||||||
|
p: number;
|
||||||
|
s: S;
|
||||||
|
a: A;
|
||||||
|
child_redemption: null;
|
||||||
|
third_party_partner: null;
|
||||||
|
log: Log[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface A {
|
||||||
|
form?: string;
|
||||||
|
OrderId: string;
|
||||||
|
CorrelationId: string;
|
||||||
|
Channel: string;
|
||||||
|
Language: string;
|
||||||
|
Country: string;
|
||||||
|
EvaluationId: string;
|
||||||
|
provider?: string;
|
||||||
|
referenceOrderID?: string;
|
||||||
|
externalRefID?: string;
|
||||||
|
denomination?: string;
|
||||||
|
rewardName?: string;
|
||||||
|
sendEmail?: string;
|
||||||
|
status?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
bal_before_deduct?: string;
|
||||||
|
bal_after_deduct?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemSnapshot {
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
price: number;
|
||||||
|
attributes: GoalItemAttributes;
|
||||||
|
config: ItemSnapshotConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemSnapshotConfig {
|
||||||
|
amount: string;
|
||||||
|
countryCode: string;
|
||||||
|
currencyCode: string;
|
||||||
|
sku: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Log {
|
||||||
|
time: Date;
|
||||||
|
from: From;
|
||||||
|
to: S;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum From {
|
||||||
|
Created = 'Created',
|
||||||
|
RiskApproved = 'RiskApproved',
|
||||||
|
RiskReview = 'RiskReview'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum S {
|
||||||
|
Cancelled = 'Cancelled',
|
||||||
|
RiskApproved = 'RiskApproved',
|
||||||
|
RiskReview = 'RiskReview',
|
||||||
|
Shipped = 'Shipped'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Profile {
|
||||||
|
ruid: string;
|
||||||
|
attributes: ProfileAttributes;
|
||||||
|
offline_attributes: OfflineAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileAttributes {
|
||||||
|
publisher: string;
|
||||||
|
publisher_upd: Date;
|
||||||
|
creative: string;
|
||||||
|
creative_upd: Date;
|
||||||
|
program: string;
|
||||||
|
program_upd: Date;
|
||||||
|
country: string;
|
||||||
|
country_upd: Date;
|
||||||
|
referrerhash: string;
|
||||||
|
referrerhash_upd: Date;
|
||||||
|
optout_upd: Date;
|
||||||
|
language: string;
|
||||||
|
language_upd: Date;
|
||||||
|
target: string;
|
||||||
|
target_upd: Date;
|
||||||
|
created: Date;
|
||||||
|
created_upd: Date;
|
||||||
|
epuid: string;
|
||||||
|
epuid_upd: Date;
|
||||||
|
goal: string;
|
||||||
|
goal_upd: Date;
|
||||||
|
waitlistattributes: string;
|
||||||
|
waitlistattributes_upd: Date;
|
||||||
|
serpbotscore_upd: Date;
|
||||||
|
iscashbackeligible: string;
|
||||||
|
cbedc: string;
|
||||||
|
rlscpct_upd: Date;
|
||||||
|
give_user: string;
|
||||||
|
rebcpc_upd: Date;
|
||||||
|
SerpBotScore_upd: Date;
|
||||||
|
AdsBotScore_upd: Date;
|
||||||
|
dbs_upd: Date;
|
||||||
|
rbs: string;
|
||||||
|
rbs_upd: Date;
|
||||||
|
iris_segmentation: string;
|
||||||
|
iris_segmentation_upd: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OfflineAttributes {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Promotion {
|
||||||
|
name: string;
|
||||||
|
priority: number;
|
||||||
|
attributes: { [key: string]: string };
|
||||||
|
tags: Tag[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Tag {
|
||||||
|
AllowTrialUser = 'allow_trial_user',
|
||||||
|
ExcludeGivePcparent = 'exclude_give_pcparent',
|
||||||
|
ExcludeGlobalConfig = 'exclude_global_config',
|
||||||
|
ExcludeHidden = 'exclude_hidden',
|
||||||
|
LOCString = 'locString',
|
||||||
|
NonGlobalConfig = 'non_global_config'
|
||||||
|
}
|
||||||
40
src/interface/Config.ts
Normal file
40
src/interface/Config.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export interface Config {
|
||||||
|
baseURL: string;
|
||||||
|
sessionPath: string;
|
||||||
|
headless: boolean;
|
||||||
|
runOnZeroPoints: boolean;
|
||||||
|
clusters: number;
|
||||||
|
workers: Workers;
|
||||||
|
globalTimeout: number;
|
||||||
|
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;
|
||||||
|
doDailyCheckIn: boolean;
|
||||||
|
doReadToEarn: boolean;
|
||||||
|
}
|
||||||
9
src/interface/OAuth.ts
Normal file
9
src/interface/OAuth.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface OAuth {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
foci: string;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
32
src/run_daily.sh
Normal file
32
src/run_daily.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Set up environment variables
|
||||||
|
export PATH=$PATH:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
|
||||||
|
|
||||||
|
# Ensure TZ is set
|
||||||
|
export TZ=${TZ}
|
||||||
|
|
||||||
|
# Change directory to the application directory
|
||||||
|
cd /usr/src/microsoft-rewards-script
|
||||||
|
|
||||||
|
# Define the minimum and maximum wait times in seconds
|
||||||
|
MINWAIT=$((5*60)) # 5 minutes
|
||||||
|
MAXWAIT=$((50*60)) # 50 minutes
|
||||||
|
|
||||||
|
# Calculate a random sleep time within the specified range
|
||||||
|
SLEEPTIME=$((MINWAIT + RANDOM % (MAXWAIT - MINWAIT)))
|
||||||
|
|
||||||
|
# Convert the sleep time to minutes for logging
|
||||||
|
SLEEP_MINUTES=$((SLEEPTIME / 60))
|
||||||
|
|
||||||
|
# Log the sleep duration
|
||||||
|
echo "Sleeping for $SLEEP_MINUTES minutes ($SLEEPTIME seconds)..."
|
||||||
|
|
||||||
|
# Sleep for the calculated time
|
||||||
|
sleep $SLEEPTIME
|
||||||
|
|
||||||
|
# Log the start of the script
|
||||||
|
echo "Starting script..."
|
||||||
|
|
||||||
|
# Execute the Node.js script directly
|
||||||
|
npm run start
|
||||||
43
src/updateConfig.js
Executable file
43
src/updateConfig.js
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const configPath = path.join(__dirname, '../dist/config.json')
|
||||||
|
|
||||||
|
// Read the existing config file
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
||||||
|
|
||||||
|
// Update the config with environment variables if they are set
|
||||||
|
config.baseURL = process.env.BASE_URL || config.baseURL
|
||||||
|
config.sessionPath = process.env.SESSION_PATH || config.sessionPath
|
||||||
|
config.headless = process.env.HEADLESS ? process.env.HEADLESS === 'true' : config.headless
|
||||||
|
config.runOnZeroPoints = process.env.RUN_ON_ZERO_POINTS ? process.env.RUN_ON_ZERO_POINTS === 'true' : config.runOnZeroPoints
|
||||||
|
config.clusters = process.env.CLUSTERS ? parseInt(process.env.CLUSTERS, 10) : config.clusters
|
||||||
|
config.saveFingerprint = process.env.SAVE_FINGERPRINT ? process.env.SAVE_FINGERPRINT === 'true' : config.saveFingerprint
|
||||||
|
config.globalTimeout = process.env.GLOBAL_TIMEOUT ? parseInt(process.env.GLOBAL_TIMEOUT, 10) : config.globalTimeout
|
||||||
|
|
||||||
|
config.workers.doDailySet = process.env.DO_DAILY_SET ? process.env.DO_DAILY_SET === 'true' : config.workers.doDailySet
|
||||||
|
config.workers.doMorePromotions = process.env.DO_MORE_PROMOTIONS ? process.env.DO_MORE_PROMOTIONS === 'true' : config.workers.doMorePromotions
|
||||||
|
config.workers.doPunchCards = process.env.DO_PUNCH_CARDS ? process.env.DO_PUNCH_CARDS === 'true' : config.workers.doPunchCards
|
||||||
|
config.workers.doDesktopSearch = process.env.DO_DESKTOP_SEARCH ? process.env.DO_DESKTOP_SEARCH === 'true' : config.workers.doDesktopSearch
|
||||||
|
config.workers.doMobileSearch = process.env.DO_MOBILE_SEARCH ? process.env.DO_MOBILE_SEARCH === 'true' : config.workers.doMobileSearch
|
||||||
|
config.workers.doDailyCheckIn = process.env.DO_DAILY_CHECK_IN ? process.env.DO_DAILY_CHECK_IN === 'true' : config.workers.doDailyCheckIn
|
||||||
|
config.workers.doReadToEarn = process.env.DO_READ_TO_EARN ? process.env.DO_READ_TO_EARN === 'true' : config.workers.doReadToEarn
|
||||||
|
|
||||||
|
config.searchSettings.useGeoLocaleQueries = process.env.USE_GEO_LOCALE_QUERIES ? process.env.USE_GEO_LOCALE_QUERIES === 'true' : config.searchSettings.useGeoLocaleQueries
|
||||||
|
config.searchSettings.scrollRandomResults = process.env.SCROLL_RANDOM_RESULTS ? process.env.SCROLL_RANDOM_RESULTS === 'true' : config.searchSettings.scrollRandomResults
|
||||||
|
config.searchSettings.clickRandomResults = process.env.CLICK_RANDOM_RESULTS ? process.env.CLICK_RANDOM_RESULTS === 'true' : config.searchSettings.clickRandomResults
|
||||||
|
config.searchSettings.searchDelay.min = process.env.SEARCH_DELAY_MIN ? parseInt(process.env.SEARCH_DELAY_MIN, 10) : config.searchSettings.searchDelay.min
|
||||||
|
config.searchSettings.searchDelay.max = process.env.SEARCH_DELAY_MAX ? parseInt(process.env.SEARCH_DELAY_MAX, 10) : config.searchSettings.searchDelay.max
|
||||||
|
config.searchSettings.retryMobileSearch = process.env.RETRY_MOBILE_SEARCH ? process.env.RETRY_MOBILE_SEARCH === 'true' : config.searchSettings.retryMobileSearch
|
||||||
|
|
||||||
|
config.webhook.enabled = process.env.WEBHOOK_ENABLED ? process.env.WEBHOOK_ENABLED === 'true' : config.webhook.enabled
|
||||||
|
config.webhook.url = process.env.WEBHOOK_URL || config.webhook.url
|
||||||
|
|
||||||
|
// Write the updated config back to the file
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
||||||
|
console.log('Config file updated with environment variables')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to write updated config file to ${configPath}:`, error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
108
src/util/Load.ts
Normal file
108
src/util/Load.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { BrowserContext, 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, browser: BrowserContext, email: string, isMobile: boolean): Promise<string> {
|
||||||
|
try {
|
||||||
|
const cookies = await browser.cookies()
|
||||||
|
|
||||||
|
// 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'
|
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 = ''
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
export async function wait(ms: number): Promise<void> {
|
export default class Util {
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, ms)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFormattedDate(ms = Date.now()): string {
|
async wait(ms: number): Promise<void> {
|
||||||
const today = new Date(ms)
|
return new Promise<void>((resolve) => {
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0
|
setTimeout(resolve, ms)
|
||||||
const day = String(today.getDate()).padStart(2, '0')
|
})
|
||||||
const year = today.getFullYear()
|
}
|
||||||
|
|
||||||
return `${month}/${day}/${year}`
|
getFormattedDate(ms = Date.now()): string {
|
||||||
}
|
const today = new Date(ms)
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0
|
||||||
export function shuffleArray<T>(array: T[]): T[] {
|
const day = String(today.getDate()).padStart(2, '0')
|
||||||
const shuffledArray = array.slice()
|
const year = today.getFullYear()
|
||||||
|
|
||||||
shuffledArray.sort(() => Math.random() - 0.5)
|
return `${month}/${day}/${year}`
|
||||||
|
}
|
||||||
return shuffledArray
|
|
||||||
}
|
shuffleArray<T>(array: T[]): T[] {
|
||||||
|
const shuffledArray = array.slice()
|
||||||
export function randomNumber(min: number, max: number): number {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
shuffledArray.sort(() => Math.random() - 0.5)
|
||||||
}
|
|
||||||
|
return shuffledArray
|
||||||
export function chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
}
|
||||||
const chunkSize = Math.ceil(arr.length / numChunks)
|
|
||||||
const chunks: T[][] = []
|
randomNumber(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
}
|
||||||
const chunk = arr.slice(i, i + chunkSize)
|
|
||||||
chunks.push(chunk)
|
chunkArray<T>(arr: T[], numChunks: number): T[][] {
|
||||||
|
const chunkSize = Math.ceil(arr.length / numChunks)
|
||||||
|
const chunks: T[][] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
|
const chunk = arr.slice(i, i + chunkSize)
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
return chunks
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
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 = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: webhook.url,
|
url: webhook.url,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user