35 Commits

Author SHA1 Message Date
TheNetsky
ce2a72ee36 1.4.8 2024-08-18 15:06:44 +02:00
CNOCM
755237caa1 Skip locked Promotions (#132) 2024-08-18 14:37:56 +02:00
HMCDAT
2b4cd505c0 Support passwordless auth (#129)
* support passwordless auth (using Authenticator app)

* update readme
2024-07-24 15:01:45 +02:00
Nworm
a39a861dab Update Search.ts (#127) 2024-07-11 14:43:26 +02:00
mgrimace
8d19129906 Docker: improved env var handling (#113)
* Improve env var handling, clarify instructions

updateConfig.js will update dist/config.json with any values specified in the docker compose file as environmental variables (env vars). If not specified it will use the default values in src/config.json (the 'usual' place where folks can customize their config).

A user can make changes to an env var (e.g., disabling Scroll Random Results), then docker compose up -d to quickly restart the container with the change.

* minor update to env vars in table

Make sure to change your compose so the updated flattened values work.

* TZ handling for cron runs of the script

docker logs netsky should now show the proper time zone for script runs that were initiated via cron schedule.
2024-06-01 14:50:29 +02:00
TheNetsky
c6ab80fe54 Delete start.bat
oops
2024-05-28 11:39:34 +02:00
TheNetsky
9b1eed526f 1.4.7
- Updated packages
- Added timeout after scrolling back to top
- Some Docker improvement thanks to @mgrimace
2024-05-28 11:38:49 +02:00
mgrimace
9a144b2e60 Docker with env var support and containerized cron scheduling (#107)
* Script will run on container start, and continue to run daily at 5:00 am.

The goal of this update was to add in-built cronjob support so that the script runs immediately on container start, then will schedule the script to run the daily at 5:00 am without requiring the user to manually create a cronjob. After starting the container and completing the first run, the container should remain active (running) but idle until the next scheduled run. Ideally, the next step will be to include support for environmental variables that will allow the user to customize when the script scheduled runs occur within the docker compose.yaml.

* added --build to docker compose command instructions

building/rebuilding the image (and the script) may reduce problems if folks make changes to their configs

* Updated the crontab default to run twice daily by default, 5am and 11am

* Update crontab

Newline needed

* Environmental variable support, custom scheduling

This is a massive update that includes docker environmental variable support for all config options. If specified, the custom env var value will supersede the default values in the config.json. You do not need to change the values in the config.json if you set a custom env var. For example, if you have headless=false in the config.json, but set it as true in the compose.yaml, it will run as HEADLESS=true. If you do not specify a particular env var, the script will automatically use the default value. Please test and contribute wherever possible!

* Fix scheduled runs, tidied up readme

* timezone support

Minor adjustment to attempt to fix timezone env var support

* Run on start, npm script env

Hopefully two fixes here, the container should again, by default, run on start, then continue per schedule. This defaults to true; however, can be optionally over-ridden in the compose.yaml with the env var RUN_ON_START=false. If set to false, the script should only run on the specified cron schedule. I also passed the TZ write into the npm start command, rather than just the container overall so it should show the proper time.

* Keep docker running after run_on_start to wait for cron

Accidentally set it up like a run_once, this should keep the container active after run_on_start to wait for the next schedule.

* improve order of cmd

This should (1) set time zone for script, (2) run once if set, then (3) idle for scheduled cron job. It was a little hacky before, this should be tidier.

* updated compose to auto-restart container

Noticed the container was exited this am, likely after automated updated to system, adding restart: unless-stopped to compose should keep container running in such circumstances.

* Update env vars in readme
2024-05-16 19:28:55 +02:00
Netsky
28b1881642 Update README.md 2024-05-09 12:38:49 +02:00
TheNetsky
ef6ad569ff 1.4.6 2024-05-09 10:04:11 +02:00
Netsky
da9ba91c5c Merge pull request #98 from jordyamc/main
Bug when detecting earnable points from more promotions
2024-05-07 15:12:10 +02:00
Jordy Mendoza
deb2d58b1b - Fix bug where the script was using x.activityType instead of x.promotionType this was causing that the script wasn't detecting earnable points from more promotions 2024-04-21 23:50:33 -06:00
Netsky
66a82c2584 Merge pull request #89 from mgrimace/docker
Clarify docker instructions, add basic compose.yaml support
2024-04-04 16:43:57 +02:00
mgrimace
8a022d5983 Clarify docker instructions, add basic compose.yaml support
This should help folks get started with docker and hopefully make it easier for testing
2024-04-03 17:59:16 -04:00
TheNetsky
64048e35d7 1.4.5
- Fix login not working
- Sessions are now saved after logging in
- Check if promotions, and searches are available before counting total point amount
2024-03-01 12:19:48 +01:00
TheNetsky
cf7f7ac790 Merge branch 'main' of https://github.com/TheNetsky/Microsoft-Rewards-Script 2024-02-05 11:47:50 +01:00
TheNetsky
f7aa5039f9 1.4.4
- Fixed login getting stuck
- Updated packages
- Fixed some errors not throwing
2024-02-05 11:47:48 +01:00
Netsky
e082fb03f0 Merge pull request #56 from Omniphantasm/patch-1
Update README.md
2024-01-26 18:55:10 +01:00
Omniphantasm
0303b8c605 Update README.md
Script is kill-chrome-win, changed readme to reflect that.
2024-01-26 12:35:30 -05:00
TheNetsky
2fea17c415 1.4.3
- Fixed browser generating mobile browser after the first account
- Hopefully fixed script erroring after not finding parent promotions
- Added support for MacOS (Thanks @alecm858)
- Made some changes to search result scrolling
2024-01-20 12:11:02 +01:00
Netsky
c5beccb54b Update README.md 2024-01-17 22:23:55 +01:00
TheNetsky
b566ccaece 1.4.2
- Scroll back to top before searching
2024-01-17 18:50:43 +01:00
TheNetsky
15b2b827eb 1.4.1
- Fixed typos
- Added Dockerfile (experimental)
- Increased scroll amount
2024-01-14 14:09:57 +01:00
Netsky
02518ee4ba Update README.md 2024-01-07 17:00:23 +01:00
Netsky
69819b5631 Add config explaination 2024-01-07 16:59:15 +01:00
Netsky
b389b87792 Set runOnZeroPoints to false by default 2024-01-07 16:38:47 +01:00
TheNetsky
9ea7f5c452 1.4.0
- Switched from Puppeteer to Playwright
- Fixed mobile searches not working
- Added fingerprint saving in config
- Added mobile search retry in config
2024-01-07 16:33:48 +01:00
TheNetsky
f3fb641ecd 1.3.2
- Better generation of mobile user agents
2023-12-27 11:38:52 +01:00
TheNetsky
bca1e7c896 1.3.1
- Poll clicking will be attempted regardless of the selector being found
- Fix tabs sticking upon search error
- Updated dependencies
2023-12-20 13:03:08 +01:00
Netsky
fd7c8e36d4 Update README.md 2023-12-01 12:12:26 +01:00
Netsky
dcb0c25d46 Remove the feature of Promo Items 2023-12-01 12:02:17 +01:00
TheNetsky
a8cb5482d4 1.3.0 2023-12-01 11:49:14 +01:00
TheNetsky
28286ff9fe Pre 1.3.0 2023-11-23 14:38:58 +01:00
TheNetsky
1d6167aeca Missing await 2023-11-04 16:24:25 +01:00
TheNetsky
3b15fe19a7 1.2.5 2023-11-04 16:21:56 +01:00
28 changed files with 878 additions and 403 deletions

View File

@@ -28,6 +28,10 @@ module.exports = {
'error',
'never'
],
'@typescript-eslint/no-explicit-any':
['warn', {
fixToUnknown: true // This line is optional and only relevant if you are using TypeScript
}],
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'error',
'prefer-arrow-callback': 'error'

3
.gitignore vendored
View File

@@ -5,4 +5,5 @@ package-lock.json
accounts.json
notes
accounts.dev.json
accounts.main.json
accounts.main.json
.DS_Store

20
.vscode/launch.json vendored
View File

@@ -1,20 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${file}",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}

40
Dockerfile Normal file
View 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'

View File

@@ -1,5 +1,5 @@
# Microsoft-Rewards-Script
Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Puppeteer.
Automated Microsoft Rewards script, however this time using TypeScript, Cheerio and Playwright.
Under development, however mainly for personal use!
@@ -11,10 +11,79 @@ Under development, however mainly for personal use!
5. Run `npm run build` to build the script
6. Run `npm run start` to start the built script
## Notes ##
- If you end the script without closing the browser window first (only with headless as false), you'll be left with hanging chrome instances using resources. Use taskmanager to kill these or use the included `npm run 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 ##
- [x] Multi-Account Support
- [x] Session Storing
- [x] 2FA Support
- [x] Passwordless Support
- [x] Headless Support
- [x] Discord Webhook Support
- [x] Desktop Searches
@@ -31,13 +100,14 @@ Under development, however mainly for personal use!
- [x] Completing Click Rewards
- [x] Completing Polls
- [x] Completing Punchcards
- [ ] Solving This Or That Quiz
- [x] Clicking Promotional Items
- [x] Solving This Or That Quiz (Random)
- [x] Solving ABC Quiz
- [ ] Completing Shopping Game
- [ ] Completing Gaming Tab
- [x] Clustering Support
- [x] Proxy Support
- [x] Docker Support (experimental)
- [x] Automatic scheduling (via Docker)
## Disclaimer ##
Your account may be at risk of getting banned or suspended using this script, you've been warned!

13
compose.yaml Normal file
View 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

View File

@@ -1,6 +1,6 @@
{
"name": "microsoft-rewards-script",
"version": "1.2.4",
"version": "1.4.8",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
@@ -10,7 +10,9 @@
"build": "tsc",
"start": "node ./dist/index.js",
"ts-start": "ts-node ./src/index.ts",
"dev": "ts-node ./src/index.ts -dev"
"dev": "ts-node ./src/index.ts -dev",
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
"create-docker": "docker build -t microsoft-rewards-script-docker ."
},
"keywords": [
"Bing Rewards",
@@ -18,24 +20,23 @@
"Bot",
"Script",
"TypeScript",
"Puppeteer",
"Playwright",
"Cheerio"
],
"author": "Netsky",
"license": "ISC",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.0",
"typescript": "^5.2.2"
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.5.1",
"cheerio": "^1.0.0-rc.12",
"eslint": "^8.49.0",
"eslint-plugin-modules-newline": "^0.0.6",
"fingerprint-generator": "^2.1.42",
"fingerprint-injector": "^2.1.42",
"puppeteer": "^21.4.1",
"puppeteer-extra": "^3.3.6",
"ts-node": "^10.9.1"
"axios": "^1.7.4",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.54",
"fingerprint-injector": "^2.1.54",
"playwright": "^1.46.1",
"ts-node": "^10.9.2"
}
}

View File

@@ -1,8 +1,11 @@
import puppeteer from 'puppeteer-extra'
import { FingerprintInjector } from 'fingerprint-injector'
import playwright from 'playwright'
import { BrowserContext } from 'playwright'
import { newInjectedContext } from 'fingerprint-injector'
import { FingerprintGenerator } from 'fingerprint-generator'
import { MicrosoftRewardsBot } from '../index'
import { loadSessionData, saveFingerprintData } from '../util/Load'
import { AccountProxy } from '../interface/Account'
@@ -12,7 +15,6 @@ https://botcheck.luminati.io/
http://f.vision/
*/
class Browser {
private bot: MicrosoftRewardsBot
@@ -20,38 +22,50 @@ class Browser {
this.bot = bot
}
async createBrowser(email: string, proxy: AccountProxy, isMobile: boolean) {
// const userAgent = await getUserAgent(isMobile)
const browser = await puppeteer.launch({
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
const browser = await playwright.chromium.launch({
//channel: 'msedge', // Uses Edge instead of chrome
headless: this.bot.config.headless,
userDataDir: await this.bot.browser.func.loadSesion(email),
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
args: [
'--no-sandbox',
'--mute-audio',
'--disable-setuid-sandbox',
proxy.url ? `--proxy-server=${proxy.url}:${proxy.port}` : ''
'--ignore-certificate-errors',
'--ignore-certificate-errors-spki-list',
'--ignore-ssl-errors'
]
})
const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
devices: isMobile ? ['mobile'] : ['desktop'],
operatingSystems: isMobile ? ['android'] : ['windows'],
browsers: ['edge'],
browserListQuery: 'last 2 Edge versions'
})
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
// Modify the newPage function to attach the fingerprint
const originalNewPage = browser.newPage
browser.newPage = async function () {
const page = await originalNewPage.apply(browser)
await new FingerprintInjector().attachFingerprintToPuppeteer(page, { fingerprint, headers })
return page
const fingerpint = sessionData.fingerprint ? sessionData.fingerprint : this.generateFingerprint()
const context = await newInjectedContext(browser, { fingerprint: fingerpint })
// 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)
}
return browser
this.bot.log('BROWSER', `Created browser with User-Agent: "${fingerpint.fingerprint.navigator.userAgent}"`)
return context
}
generateFingerprint() {
const fingerPrintData = new FingerprintGenerator().getFingerprint({
devices: this.bot.isMobile ? ['mobile'] : ['desktop'],
operatingSystems: this.bot.isMobile ? ['android'] : ['windows'],
browsers: ['edge']
})
return fingerPrintData
}
}
export default Browser
export default Browser

View File

@@ -1,7 +1,5 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { CheerioAPI, load } from 'cheerio'
import fs from 'fs'
import path from 'path'
import { MicrosoftRewardsBot } from '../index'
@@ -16,11 +14,20 @@ export default class BrowserFunc {
this.bot = bot
}
async goHome(page: Page): Promise<boolean> {
/**
* Navigate the provided page to rewards homepage
* @param {Page} page Playwright page
*/
async goHome(page: Page) {
try {
const dashboardURL = new URL(this.bot.config.baseURL)
if (page.url() === dashboardURL.href) {
return
}
await page.goto(this.bot.config.baseURL)
const maxIterations = 5 // Maximum iterations set to 5
@@ -30,7 +37,7 @@ export default class BrowserFunc {
await this.bot.browser.utils.tryDismissCookieBanner(page)
// Check if account is suspended
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (isSuspended) {
this.bot.log('GO-HOME', 'This account is suspended!', 'error')
throw new Error('Account has been suspended!')
@@ -39,12 +46,14 @@ export default class BrowserFunc {
try {
// If activities are found, exit the loop
await page.waitForSelector('#more-activities', { timeout: 1000 })
this.bot.log('GO-HOME', 'Visited homepage successfully')
break
} catch (error) {
// Continue if element is not found
}
// Below runs if the homepage was unable to be visited
const currentURL = new URL(page.url())
if (currentURL.hostname !== dashboardURL.hostname) {
@@ -52,60 +61,134 @@ export default class BrowserFunc {
await this.bot.utils.wait(2000)
await page.goto(this.bot.config.baseURL)
} else {
this.bot.log('GO-HOME', 'Visited homepage successfully')
break
}
await this.bot.utils.wait(5000)
this.bot.log('GO-HOME', 'Visited homepage successfully')
}
} catch (error) {
console.error('An error occurred:', error)
return false
throw this.bot.log('GO-HOME', 'An error occurred:' + error, 'error')
}
return true
}
async getDashboardData(page: Page): Promise<DashboardData> {
/**
* Fetch user dashboard data
* @returns {DashboardData} Object of user bing rewards dashboard data
*/
async getDashboardData(): Promise<DashboardData> {
const dashboardURL = new URL(this.bot.config.baseURL)
const currentURL = new URL(page.url())
const currentURL = new URL(this.bot.homePage.url())
// Should never happen since tasks are opened in a new tab!
if (currentURL.hostname !== dashboardURL.hostname) {
this.bot.log('DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
await this.goHome(page)
await this.goHome(this.bot.homePage)
}
// Reload the page to get new data
await page.reload({ waitUntil: 'networkidle2' })
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
const scriptContent = await page.evaluate(() => {
const scriptContent = await this.bot.homePage.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
if (targetScript) {
return targetScript.innerText
} else {
throw this.bot.log('GET-DASHBOARD-DATA', 'Script containing dashboard data not found', 'error')
}
return targetScript?.innerText ? targetScript.innerText : null
})
if (!scriptContent) {
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
}
// Extract the dashboard object from the script content
const dashboardData = await page.evaluate(scriptContent => {
const dashboardData = await this.bot.homePage.evaluate(scriptContent => {
// Extract the dashboard object using regex
const regex = /var dashboard = (\{.*?\});/s
const match = regex.exec(scriptContent)
if (match && match[1]) {
return JSON.parse(match[1])
} else {
throw this.bot.log('GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
}
}, 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
* @returns {number} Total earnable points
*/
async getEarnablePoints(): Promise<number> {
try {
const data = await this.getDashboardData()
// These only include the points from tasks that the script can complete!
let totalEarnablePoints = 0
// Desktop Search Points
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-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()
@@ -135,78 +218,9 @@ export default class BrowserFunc {
}
async getSearchPoints(page: Page): Promise<Counters> {
const dashboardData = await this.getDashboardData(page) // Always fetch newest data
return dashboardData.userStatus.counters
}
async getEarnablePoints(data: DashboardData, page: null | Page = null): Promise<number> {
try {
// Fetch new data if page is provided
if (page) {
data = await this.getDashboardData(page)
}
// These only include the points from tasks that the script can complete!
let totalEarnablePoints = 0
// Desktop Search Points
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
// Mobile Search Points
if (data.userStatus.counters.mobileSearch?.length) {
data.userStatus.counters.mobileSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
}
// Daily Set
data.dailySetPromotions[this.bot.utils.getFormattedDate()]?.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
// More Promotions
data.morePromotions.forEach(x => {
// Only count points from supported activities
if (['quiz', 'urlreward'].includes(x.activityType)) {
totalEarnablePoints += (x.pointProgressMax - x.pointProgress)
}
})
return totalEarnablePoints
} catch (error) {
throw this.bot.log('GET-EARNABLE-POINTS', 'An error occurred:' + error, 'error')
}
}
async getCurrentPoints(data: DashboardData, page: null | Page = null): Promise<number> {
try {
// Fetch new data if page is provided
if (page) {
data = await this.getDashboardData(page)
}
return data.userStatus.availablePoints
} catch (error) {
throw this.bot.log('GET-CURRENT-POINTS', 'An error occurred:' + error, 'error')
}
}
async loadSesion(email: string): Promise<string> {
const sessionDir = path.join(__dirname, this.bot.config.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)
}
}
async waitForQuizRefresh(page: Page): Promise<boolean> {
try {
await page.waitForSelector('#rqHeaderCredits', { visible: true, timeout: 10_000 })
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 })
await this.bot.utils.wait(2000)
return true
@@ -218,7 +232,7 @@ export default class BrowserFunc {
async checkQuizCompleted(page: Page): Promise<boolean> {
try {
await page.waitForSelector('#quizCompleteContainer', { visible: true, timeout: 2000 })
await page.waitForSelector('#quizCompleteContainer', { state: 'visible', timeout: 2000 })
await this.bot.utils.wait(2000)
return true

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { MicrosoftRewardsBot } from '../index'
@@ -12,6 +12,7 @@ export default class BrowserUtil {
async tryDismissAllMessages(page: Page): Promise<boolean> {
const buttons = [
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
{ selector: '#iShowSkip', label: 'iShowSkip' },
{ selector: '#iNext', label: 'iNext' },
@@ -24,7 +25,7 @@ export default class BrowserUtil {
for (const button of buttons) {
try {
const element = await page.waitForSelector(button.selector, { visible: true, timeout: 1000 })
const element = await page.waitForSelector(button.selector, { state: 'visible', timeout: 1000 })
if (element) {
await element.click()
result = true
@@ -73,8 +74,8 @@ export default class BrowserUtil {
try {
await this.bot.utils.wait(500)
const browser = page.browser()
const pages = await browser.pages()
const browser = page.context()
const pages = browser.pages()
const newTab = pages[pages.length - 1]
if (newTab) {
@@ -87,4 +88,38 @@ export default class BrowserUtil {
}
}
async getTabs(page: Page) {
try {
const browser = page.context()
const pages = browser.pages()
const homeTab = pages[1]
let homeTabURL: URL
if (!homeTab) {
throw this.bot.log('GET-TABS', 'Home tab could not be found!', 'error')
} else {
homeTabURL = new URL(homeTab.url())
if (homeTabURL.hostname !== 'rewards.bing.com') {
throw this.bot.log('GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
}
}
const workerTab = pages[2]
if (!workerTab) {
throw this.bot.log('GET-TABS', 'Worker tab could not be found!', 'error')
}
return {
homeTab: homeTab,
workerTab: workerTab
}
} catch (error) {
throw this.bot.log('GET-TABS', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -4,6 +4,7 @@
"headless": false,
"runOnZeroPoints": false,
"clusters": 1,
"saveFingerprint": false,
"workers": {
"doDailySet": true,
"doMorePromotions": true,
@@ -11,6 +12,7 @@
"doDesktopSearch": true,
"doMobileSearch": true
},
"globalTimeout": 30000,
"searchSettings": {
"useGeoLocaleQueries": false,
"scrollRandomResults": true,
@@ -18,10 +20,12 @@
"searchDelay": {
"min": 10000,
"max": 20000
}
},
"retryMobileSearch": true
},
"webhook": {
"enabled": false,
"url": ""
}
},
"cronStartTime": "0 5,11 * * *"
}

1
src/crontab.template Normal file
View 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

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { MicrosoftRewardsBot } from '../index'
@@ -19,9 +19,9 @@ export default class Activities {
this.bot = bot
}
doSearch = async (page: Page, data: DashboardData, mobile: boolean): Promise<void> => {
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
const search = new Search(this.bot)
await search.doSearch(page, data, mobile)
await search.doSearch(page, data)
}
doABC = async (page: Page): Promise<void> => {

View File

@@ -1,7 +1,8 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import readline from 'readline'
import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
const rl = readline.createInterface({
input: process.stdin,
@@ -20,19 +21,18 @@ export class Login {
try {
// Navigate to the Bing login page
await page.goto('https://login.live.com/')
await page.goto('https://rewards.bing.com/signin')
const isLoggedIn = await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false)
if (!isLoggedIn) {
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { visible: true, timeout: 10_000 }).then(() => true).catch(() => false)
// Check if account is locked
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 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 page.waitForSelector('#loginHeader', { visible: true, timeout: 10_000 })
await this.execLogin(page, email, password)
this.bot.log('LOGIN', 'Logged into Microsoft successfully')
} else {
@@ -42,6 +42,9 @@ export class Login {
// Check if logged in to bing
await this.checkBingLogin(page)
// Save session
await saveSessionData(this.bot.config.sessionPath, page.context(), email, this.bot.isMobile)
// We're done logging in
this.bot.log('LOGIN', 'Logged in successfully')
@@ -52,44 +55,111 @@ export class Login {
}
private async execLogin(page: Page, email: string, password: string) {
await page.type('#i0116', email)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully')
try {
await page.waitForSelector('#i0118', { visible: true, timeout: 2000 })
await this.bot.utils.wait(2000)
await page.type('#i0118', password)
await page.click('#idSIButton9')
} catch (error) {
this.bot.log('LOGIN', '2FA code required')
const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.type('input[name="otc"]', code)
await page.keyboard.press('Enter')
} finally {
this.bot.log('LOGIN', 'Password entered successfully')
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')
}
}
const currentURL = new URL(page.url())
private async enterEmail(page: Page, email: string) {
await page.fill('#i0116', email)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully')
}
while (currentURL.pathname !== '/' || currentURL.hostname !== 'account.microsoft.com') {
private async enterPassword(page: Page, password: string) {
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) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
this.bot.log('LOGIN', '2FA code entered successfully')
}
private async checkLoggedIn(page: Page) {
const targetHostname = 'rewards.bing.com'
const targetPathname = '/'
// eslint-disable-next-line no-constant-condition
while (true) {
await this.bot.browser.utils.tryDismissAllMessages(page)
currentURL.href = page.url()
const currentURL = new URL(page.url())
if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) {
break
}
}
// Wait for login to complete
await page.waitForSelector('html[data-role-name="MeePortal"]', { timeout: 10_000 })
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 })
this.bot.log('LOGIN', 'Successfully logged into the rewards portal')
}
private async checkBingLogin(page: Page): Promise<void> {
@@ -103,11 +173,11 @@ export class Login {
const currentUrl = new URL(page.url())
if (currentUrl.hostname === 'www.bing.com' && currentUrl.pathname === '/') {
await this.bot.utils.wait(3000)
await this.bot.browser.utils.tryDismissBingCookieBanner(page)
const loggedIn = await this.checkBingLoginStatus(page)
if (loggedIn) {
// If mobile browser, skip this step
if (loggedIn || this.bot.isMobile) {
this.bot.log('LOGIN-BING', 'Bing login verification passed!')
break
}
@@ -123,7 +193,7 @@ export class Login {
private async checkBingLoginStatus(page: Page): Promise<boolean> {
try {
await page.waitForSelector('#id_n', { timeout: 10_000 })
await page.waitForSelector('#id_n', { timeout: 5000 })
return true
} catch (error) {
return false

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
@@ -27,13 +27,18 @@ export class Workers {
await this.solveActivities(page, activitiesUncompleted)
page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log('DAILY-SET', 'All "Daily Set" items have been completed')
}
// Punch Card
async doPunchCard(page: Page, data: DashboardData) {
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
const punchCardsUncompleted = data.punchCards?.filter(x => !x.parentPromotion?.complete) ?? [] // Only return uncompleted punch cards
if (!punchCardsUncompleted.length) {
this.bot.log('PUNCH-CARD', 'All "Punch Cards" have already been completed')
@@ -41,21 +46,31 @@ export class Workers {
}
for (const punchCard of punchCardsUncompleted) {
// Get latest page for each card
page = await this.bot.browser.utils.getLatestTab(page)
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
// Solve Activities
this.bot.log('PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
const browser = page.browser()
page = await browser.newPage()
// Got to punch card index page in a new tab
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
// Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { })
await this.solveActivities(page, activitiesUncompleted, punchCard)
// Close the punch card index page
await page.close()
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close()
} else {
await this.bot.browser.func.goHome(page)
}
this.bot.log('PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
}
@@ -72,7 +87,7 @@ export class Workers {
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
}
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
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')
@@ -80,47 +95,65 @@ export class Workers {
}
// Solve Activities
this.bot.log('MORE-PROMOTIONS', 'Started solving "More Promotions" item')
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 all the different types of activities
private async solveActivities(page: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
for (const activity of activities) {
try {
// Reselect the worker page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
let selector = `[data-bi-id="${activity.offerId}"]`
const pages = activityPage.context().pages()
if (pages.length > 3) {
await activityPage.close()
if (punchCard) {
selector = await this.bot.browser.func.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}"]`
}
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
}
// Wait for element to load
await page.waitForSelector(selector, { timeout: 10_000 })
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 page.click(selector)
// Cooldown
await this.bot.utils.wait(4000)
await activityPage.click(selector)
// Select the new activity page
const activityPage = await this.bot.browser.utils.getLatestTab(page)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Wait for body to load
await activityPage.waitForSelector('body', { timeout: 10_000 })
// Wait for the new tab to fully load, ignore error.
/*
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load.
*/
await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { })
await this.bot.utils.wait(5000)
switch (activity.promotionType) {
// Quiz (Poll, Quiz or ABC)
@@ -167,9 +200,11 @@ export class Workers {
// Cooldown
await this.bot.utils.wait(2000)
} catch (error) {
this.bot.log('ACTIVITY', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { Workers } from '../Workers'
@@ -15,18 +15,18 @@ export class ABC extends Workers {
const maxIterations = 15
let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { visible: true, timeout: 10_000 })
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 })
const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { visible: true, timeout: 10_000 })
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 })
await this.bot.utils.wait(2000)
await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { visible: true, timeout: 10_000 })
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 })
await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { Workers } from '../Workers'
@@ -11,7 +11,7 @@ export class Poll extends Workers {
try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { visible: true, timeout: 10_000 })
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not
await this.bot.utils.wait(2000)
await page.click(buttonId)

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { Workers } from '../Workers'
@@ -10,7 +10,7 @@ export class Quiz extends Workers {
try {
// Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { visible: true, timeout: 3000 }).then(() => true).catch(() => false)
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (quizNotStarted) {
await page.click('#rqStartQuiz')
} else {
@@ -29,7 +29,7 @@ export class Quiz extends Workers {
const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
@@ -39,7 +39,7 @@ export class Quiz extends Workers {
// Click the answers
for (const answer of answers) {
await page.waitForSelector(answer, { visible: true, timeout: 2000 })
await page.waitForSelector(answer, { state: 'visible', timeout: 2000 })
// Click the answer on page
await page.click(answer)
@@ -52,14 +52,14 @@ export class Quiz extends Workers {
}
}
// Other type quiz
// Other type quiz, lightspeed
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
const correctOption = quizData.correctAnswer
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { visible: true, timeout: 10_000 })
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 })
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
if (dataOption === correctOption) {

View File

@@ -1,9 +1,10 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import axios from 'axios'
import { platform } from 'os'
import { Workers } from '../Workers'
import { DashboardData, DashboardImpression } from '../../interface/DashboardData'
import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleTrends } from '../../interface/GoogleDailyTrends'
import { GoogleSearch } from '../../interface/Search'
@@ -12,19 +13,16 @@ export class Search extends Workers {
private searchPageURL = 'https://bing.com'
public async doSearch(page: Page, data: DashboardData, mobile: boolean) {
public async doSearch(page: Page, data: DashboardData) {
this.bot.log('SEARCH-BING', 'Starting bing searches')
const mobileData = data.userStatus.counters?.mobileSearch ? data.userStatus.counters.mobileSearch[0] : null // Mobile searches
const edgeData = data.userStatus.counters.pcSearch[1] as DashboardImpression // Edge searches
const genericData = data.userStatus.counters.pcSearch[0] as DashboardImpression // Normal searches
page = await this.bot.browser.utils.getLatestTab(page)
let missingPoints = (mobile && mobileData) ?
(mobileData.pointProgressMax - mobileData.pointProgress) :
(edgeData.pointProgressMax - edgeData.pointProgress) + (genericData.pointProgressMax - genericData.pointProgress)
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
let missingPoints = this.calculatePoints(searchCounters)
if (missingPoints == 0) {
this.bot.log('SEARCH-BING', `Bing searches for ${mobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
if (missingPoints === 0) {
this.bot.log('SEARCH-BING', `Bing searches for ${this.bot.isMobile ? 'MOBILE' : 'DESKTOP'} have already been completed`)
return
}
@@ -35,33 +33,25 @@ export class Search extends Workers {
// Deduplicate the search terms
googleSearchQueries = [...new Set(googleSearchQueries)]
// Open a new tab
const browser = page.browser()
const searchPage = await browser.newPage()
// Go to bing
await searchPage.goto(this.searchPageURL)
await page.goto(this.searchPageURL)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search
const queries: string[] = []
googleSearchQueries.forEach(x => queries.push(x.topic, ...x.related))
// Mobile search doesn't seem to like related queries?
googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) })
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: ${mobile}`)
this.bot.log('SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query} | Mobile: ${this.bot.isMobile}`)
const newData = await this.bingSearch(page, searchPage, query)
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
const newMissingPoints = (mobile && newMobileData) ?
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
searchCounters = await this.bingSearch(page, query)
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints == missingPoints) {
@@ -72,7 +62,13 @@ export class Search extends Workers {
missingPoints = newMissingPoints
if (missingPoints == 0) {
if (missingPoints === 0) {
break
}
// Only for mobile searches
if (maxLoop > 3 && this.bot.isMobile) {
this.bot.log('SEARCH-BING-MOBILE', 'Search didn\'t gain point for 3 iterations, likely bad User-Agent', 'warn')
break
}
@@ -84,6 +80,11 @@ export class Search extends Workers {
}
}
// Only for mobile searches
if (missingPoints > 0 && this.bot.isMobile) {
return
}
// If we still got remaining search queries, generate extra ones
if (missingPoints > 0) {
this.bot.log('SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
@@ -97,16 +98,10 @@ export class Search extends Workers {
if (relatedTerms.length > 3) {
// Search for the first 2 related terms
for (const term of relatedTerms.slice(1, 3)) {
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${mobile}`)
const newData = await this.bingSearch(page, searchPage, query.topic)
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`)
const newMobileData = newData.mobileSearch ? newData.mobileSearch[0] : null // Mobile searches
const newEdgeData = newData.pcSearch[1] as DashboardImpression // Edge searches
const newGenericData = newData.pcSearch[0] as DashboardImpression // Normal searches
const newMissingPoints = (mobile && newMobileData) ?
(newMobileData.pointProgressMax - newMobileData.pointProgress) :
(newEdgeData.pointProgressMax - newEdgeData.pointProgress) + (newGenericData.pointProgressMax - newGenericData.pointProgress)
searchCounters = await this.bingSearch(page, term)
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints == missingPoints) {
@@ -118,7 +113,7 @@ export class Search extends Workers {
missingPoints = newMissingPoints
// If we satisfied the searches
if (missingPoints == 0) {
if (missingPoints === 0) {
break
}
@@ -135,18 +130,28 @@ export class Search extends Workers {
this.bot.log('SEARCH-BING', 'Completed searches')
}
private async bingSearch(page: Page, searchPage: Page, query: string) {
private async bingSearch(searchPage: Page, query: string) {
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
// Try a max of 5 times
for (let i = 0; i < 5; i++) {
try {
// Go to top of the page
await searchPage.evaluate(() => {
window.scrollTo(0, 0)
})
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { visible: true, timeout: 10_000 })
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 })
await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500)
await searchPage.keyboard.down('Control')
await searchPage.keyboard.down(platformControlKey)
await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace')
await searchPage.keyboard.up('Control')
await searchPage.keyboard.up(platformControlKey)
await searchPage.keyboard.type(query)
await searchPage.keyboard.press('Enter')
@@ -160,9 +165,10 @@ export class Search extends Workers {
await this.clickRandomLink(searchPage)
}
// 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(page)
return await this.bot.browser.func.getSearchPoints()
} catch (error) {
if (i === 5) {
@@ -173,12 +179,16 @@ export class Search extends Workers {
this.bot.log('SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
this.bot.log('SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
// Reset the tabs
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
await this.closeTabs(lastTab, this.searchPageURL)
await this.bot.utils.wait(4000)
}
}
this.bot.log('SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
return await this.bot.browser.func.getSearchPoints(page)
return await this.bot.browser.func.getSearchPoints()
}
private async getGoogleTrends(geoLocale: string, queryCount: number): Promise<GoogleSearch[]> {
@@ -211,7 +221,7 @@ export class Search extends Workers {
for (const topic of data.default.trendingSearchesDays[0]?.trendingSearches ?? []) {
queryTerms.push({
topic: topic.title.query.toLowerCase(),
related: topic.relatedQueries.map(x => x.query.toLocaleLowerCase())
related: topic.relatedQueries.map(x => x.query.toLowerCase())
})
}
@@ -252,10 +262,12 @@ export class Search extends Workers {
private async randomScroll(page: Page) {
try {
// Press the arrow down key to scroll
for (let i = 0; i < this.bot.utils.randomNumber(5, 100); i++) {
await page.keyboard.press('ArrowDown')
}
const scrollAmount = this.bot.utils.randomNumber(5, 5000)
await page.evaluate((scrollAmount) => {
window.scrollBy(0, scrollAmount)
}, scrollAmount)
} catch (error) {
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
}
@@ -267,68 +279,61 @@ export class Search extends Workers {
await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not
// Wait for website to load
await this.bot.utils.wait(3000)
// Will get current tab if no new one is created
let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Wait for the body of the new page to be loaded
await lastTab.waitForSelector('body', { timeout: 10_000 }).catch(() => { })
// Let website load, if it doesn't load within 5 sec. exit regardless
await this.bot.utils.wait(5000)
// Check if the tab is closed or not
if (!lastTab.isClosed()) {
let lastTabURL = new URL(lastTab.url()) // Get new tab info
let lastTabURL = new URL(lastTab.url()) // Get new tab info
// Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0
while (lastTabURL.href !== searchListingURL.href && i < 5) {
// If hostname is still bing, (Bing images/news etc)
if (lastTabURL.hostname == searchListingURL.hostname) {
await lastTab.goBack()
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
// If "goBack" didn't return to search listing (due to redirects)
if (lastTabURL.hostname !== searchListingURL.hostname) {
await lastTab.goto(this.searchPageURL)
}
} else { // No longer on bing, likely opened a new tab, close this tab
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
lastTabURL = new URL(lastTab.url())
const tabs = await (page.browser()).pages() // Get all tabs open
// If the browser has more than 3 tabs open, it has opened a new one, we need to close this one.
if (tabs.length > 3) {
// Make sure the page is still open!
if (!lastTab.isClosed()) {
await lastTab.close()
}
} else if (lastTabURL.href !== searchListingURL.href) {
await lastTab.goBack()
lastTab = await this.bot.browser.utils.getLatestTab(page) // Get last opened tab
lastTabURL = new URL(lastTab.url())
// If "goBack" didn't return to search listing (due to redirects)
if (lastTabURL.hostname !== searchListingURL.hostname) {
await lastTab.goto(this.searchPageURL)
}
}
}
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
i++
}
// Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0
while (lastTabURL.href !== searchListingURL.href && i < 5) {
await this.closeTabs(lastTab, searchListingURL.href)
// End of loop, refresh lastPage
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
lastTabURL = new URL(lastTab.url()) // Get new tab info
i++
}
} catch (error) {
this.bot.log('SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
}
}
}
private async closeTabs(lastTab: Page, url: string) {
const browser = lastTab.context()
const tabs = browser.pages()
// If more than 3 tabs are open, close the last tab
if (tabs.length > 2) {
await lastTab.close()
// If only 1 tab is open, open a new one to search in
} else if (tabs.length === 1) {
const newPage = await browser.newPage()
await newPage.goto(url)
// Else go back one page
} else {
await lastTab.goBack()
}
}
private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches
const edgeData = counters.pcSearch?.[1] // Edge searches
const missingPoints = (this.bot.isMobile && mobileData)
? mobileData.pointProgressMax - mobileData.pointProgress
: (edgeData ? edgeData.pointProgressMax - edgeData.pointProgress : 0)
+ (genericData ? genericData.pointProgressMax - genericData.pointProgress : 0)
return missingPoints
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { Page } from 'puppeteer'
import { Page } from 'playwright'
import { Workers } from '../Workers'
@@ -9,7 +9,8 @@ export class UrlReward extends Workers {
this.bot.log('URL-REWARD', 'Trying to complete UrlReward')
try {
// After waiting, close the page
this.bot.utils.wait(2000)
await page.close()
this.bot.log('URL-REWARD', 'Completed the UrlReward successfully')

View File

@@ -1,4 +1,5 @@
import cluster from 'cluster'
import { BrowserContext, Page } from 'playwright'
import Browser from './browser/Browser'
import BrowserFunc from './browser/BrowserFunc'
@@ -6,7 +7,7 @@ import BrowserUtil from './browser/BrowserUtil'
import { log } from './util/Logger'
import Util from './util/Utils'
import { loadAccounts, loadConfig } from './util/Load'
import { loadAccounts, loadConfig, saveSessionData } from './util/Load'
import { Login } from './functions/Login'
import { Workers } from './functions/Workers'
@@ -24,6 +25,8 @@ export class MicrosoftRewardsBot {
func: BrowserFunc,
utils: BrowserUtil
}
public isMobile: boolean = false
public homePage!: Page
private collectedPoints: number = 0
private activeWorkers: number
@@ -122,116 +125,138 @@ export class MicrosoftRewardsBot {
// Desktop
async Desktop(account: Account) {
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, false)
const page = await browser.newPage()
let pages = await browser.pages()
this.isMobile = false
// If for some reason the browser initializes with more than 2 pages, close these
while (pages.length > 2) {
await pages[0]?.close()
pages = await browser.pages()
}
// Log into proxy
await page.authenticate({ username: account.proxy.username, password: account.proxy.password })
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
this.homePage = await browser.newPage()
log('MAIN', 'Starting DESKTOP browser')
// Login into MS Rewards
await this.login.login(page, account.email, account.password)
// Login into MS Rewards, then go to rewards homepage
await this.login.login(this.homePage, account.email, account.password)
await this.browser.func.goHome(this.homePage)
const wentHome = await this.browser.func.goHome(page)
if (!wentHome) {
throw log('MAIN', 'Unable to get dashboard page', 'error')
}
const data = await this.browser.func.getDashboardData(page)
const data = await this.browser.func.getDashboardData()
log('MAIN-POINTS', `Current point count: ${data.userStatus.availablePoints}`)
const earnablePoints = await this.browser.func.getEarnablePoints(data)
const earnablePoints = await this.browser.func.getEarnablePoints()
this.collectedPoints = earnablePoints
log('MAIN-POINTS', `You can earn ${earnablePoints} points today`)
// If runOnZeroPoints is false and 0 points to earn, don't continue
if (!this.config.runOnZeroPoints && this.collectedPoints === 0) {
log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping')
log('MAIN', 'No points to earn and "runOnZeroPoints" is set to "false", stopping!')
// Close desktop browser
return await browser.close()
return await this.closeBrowser(browser, account.email)
}
// Open a new tab to where the tasks are going to be completed
const workerPage = await browser.newPage()
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// Complete daily set
if (this.config.workers.doDailySet) {
await this.workers.doDailySet(page, data)
await this.workers.doDailySet(workerPage, data)
}
// Complete more promotions
if (this.config.workers.doMorePromotions) {
await this.workers.doMorePromotions(page, data)
await this.workers.doMorePromotions(workerPage, data)
}
// Complete punch cards
if (this.config.workers.doPunchCards) {
await this.workers.doPunchCard(page, data)
await this.workers.doPunchCard(workerPage, data)
}
// Do desktop searches
if (this.config.workers.doDesktopSearch) {
await this.activities.doSearch(page, data, false)
await this.activities.doSearch(workerPage, data)
}
// Save cookies
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
// Close desktop browser
await browser.close()
await this.closeBrowser(browser, account.email)
return
}
// Mobile
async Mobile(account: Account) {
const browser = await this.browserFactory.createBrowser(account.email, account.proxy, true)
const page = await browser.newPage()
let pages = await browser.pages()
this.isMobile = true
// If for some reason the browser initializes with more than 2 pages, close these
while (pages.length > 2) {
await pages[0]?.close()
pages = await browser.pages()
}
// Log into proxy
await page.authenticate({ username: account.proxy.username, password: account.proxy.password })
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
this.homePage = await browser.newPage()
log('MAIN', 'Starting MOBILE browser')
// Login into MS Rewards
await this.login.login(page, account.email, account.password)
// Login into MS Rewards, then go to rewards homepage
await this.login.login(this.homePage, account.email, account.password)
await this.browser.func.goHome(this.homePage)
await this.browser.func.goHome(page)
const data = await this.browser.func.getDashboardData(page)
const data = await this.browser.func.getDashboardData()
// If no mobile searches data found, stop (Does not exist on new accounts)
if (!data.userStatus.counters.mobileSearch) {
log('MAIN', 'No mobile searches found, stopping')
log('MAIN', 'No mobile searches found, stopping!')
// Close mobile browser
return await browser.close()
return await this.closeBrowser(browser, account.email)
}
// Open a new tab to where the tasks are going to be completed
const workerPage = await browser.newPage()
// Go to homepage on worker page
await this.browser.func.goHome(workerPage)
// Do mobile searches
if (this.config.workers.doMobileSearch) {
await this.activities.doSearch(page, data, true)
await this.activities.doSearch(workerPage, data)
// Fetch current search points
const mobileSearchPoints = (await this.browser.func.getSearchPoints()).mobileSearch?.[0]
// If the remaining mobile points does not equal 0, restart and assume the generated UA is invalid
// Retry until all points are gathered when (retryMobileSearch is enabled)
if (this.config.searchSettings.retryMobileSearch && mobileSearchPoints && ((mobileSearchPoints.pointProgressMax - mobileSearchPoints.pointProgress) > 0)) {
log('MAIN', 'Unable to complete mobile searches, bad User-Agent? Retrying...')
// Close mobile browser
await this.closeBrowser(browser, account.email)
// Retry
await this.Mobile(account)
}
}
// Fetch new points
const earnablePoints = await this.browser.func.getEarnablePoints(data, page)
const earnablePoints = await this.browser.func.getEarnablePoints()
// If the new earnable is 0, means we got all the points, else retract
this.collectedPoints = earnablePoints === 0 ? this.collectedPoints : (this.collectedPoints - earnablePoints)
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
// Close mobile browser
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()
}
}
const bot = new MicrosoftRewardsBot()
// Initialize accounts first and then start the bot

View File

@@ -5,8 +5,10 @@ export interface Config {
runOnZeroPoints: boolean;
clusters: number;
workers: Workers;
globalTimeout: number;
searchSettings: SearchSettings;
webhook: Webhook;
saveFingerprint: boolean;
}
export interface SearchSettings {
@@ -14,6 +16,7 @@ export interface SearchSettings {
scrollRandomResults: boolean;
clickRandomResults: boolean;
searchDelay: SearchDelay;
retryMobileSearch: boolean;
}
export interface SearchDelay {

32
src/run_daily.sh Normal file
View 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

41
src/updateConfig.js Executable file
View File

@@ -0,0 +1,41 @@
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.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);
}

View File

@@ -1,6 +1,9 @@
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'
@@ -32,4 +35,74 @@ export function loadConfig(): Config {
} catch (error) {
throw new Error(error as string)
}
}
export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, getFingerprint: boolean) {
try {
// Fetch cookie file
const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`)
let cookies: Cookie[] = []
if (fs.existsSync(cookieFile)) {
const cookiesData = await fs.promises.readFile(cookieFile, 'utf-8')
cookies = JSON.parse(cookiesData)
}
// Fetch fingerprint file
const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
let fingerprint!: BrowserFingerprintWithHeaders
if (getFingerprint && fs.existsSync(fingerprintFile)) {
const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8')
fingerprint = JSON.parse(fingerprintData)
}
return {
cookies: cookies,
fingerprint: fingerprint
}
} catch (error) {
throw new Error(error as string)
}
}
export async function saveSessionData(sessionPath: string, 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)
}
}

View File

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

View File

@@ -81,7 +81,7 @@ export async function getEdgeVersions() {
export function getSystemComponents(mobile: boolean): string {
const osId: string = mobile ? 'Linux' : 'Windows NT 10.0'
const uaPlatform: string = mobile ? 'Android 13' : 'Win64; x64'
const uaPlatform: string = mobile ? `Android 1${Math.floor(Math.random() * 5)}` : 'Win64; x64'
if (mobile) {
return `${uaPlatform}; ${osId}; K`