15 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
18 changed files with 314 additions and 109 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

View File

@@ -4,21 +4,12 @@ FROM node:18
# Set the working directory in the container
WORKDIR /usr/src/microsoft-rewards-script
# Install jq
RUN apt-get update && apt-get install -y jq
# 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 . .
# Check if "headless" is set to "true" in the config.json file
# DELETE BELOW IF YOU WANT TO RUN THE DOCKER SCRIPT HEADFULL!
RUN HEADLESS=$(cat src/config.json | jq -r .headless) \
&& if [ "$HEADLESS" != "true" ]; then \
echo "Error: 'headless' in src/config.json is not true."; \
exit 1; \
fi
# Install dependencies including Playwright
RUN apt-get install -y \
xvfb \
@@ -39,5 +30,11 @@ RUN npm run build
# Install playwright chromium
RUN npx playwright install chromium
# Define the command to run your application
CMD ["npm", "start"]
# 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

@@ -16,37 +16,74 @@ Under development, however mainly for personal use!
- 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/or `config.json`
3. Run `docker build -t microsoft-rewards-script-docker .`
- Docker container has to be recreated for any changes regarding the `config.json` and/or `accounts.json`!
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 |
| :------------- |:-------------| :-----|
| baseURL | MS Rewards page | `https://rewards.bing.com` |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) |
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
| saveFingerprint | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
| workers.doDailySet | Complete daily set items | `true` |
| workers.doMorePromotions | Complete promotional items | `true` |
| workers.doPunchCards | Complete punchcards | `true` |
| workers.doDesktopSearch | Complete daily desktop searches | `true` |
| workers.doMobileSearch | Complete daily mobile searches | `true` |
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
| scrollRandomResults | Scroll randomly in search results | `true` |
| searchSettings.clickRandomResults | Visit random website from search result| `true` |
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 10000` (10 seconds) `max: 20000` (20 seconds) |
| searchSettings.retryMobileSearch | Keep retrying mobile searches until completed (indefinite)| `false` |
| webhook.enabled | Enable or disable your set webhook | `false` |
| webhook.url | Your Discord webhook URL | `null` |
| 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
@@ -69,6 +106,8 @@ Under development, however mainly for personal use!
- [ ] 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.4.4",
"version": "1.4.8",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
@@ -26,17 +26,17 @@
"author": "Netsky",
"license": "ISC",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.20.0",
"eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.3.3"
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.6.7",
"cheerio": "^1.0.0-rc.12",
"fingerprint-generator": "^2.1.48",
"fingerprint-injector": "^2.1.48",
"playwright": "^1.41.2",
"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

@@ -43,6 +43,9 @@ class Browser {
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) {

View File

@@ -142,7 +142,9 @@ export default class BrowserFunc {
let totalEarnablePoints = 0
// Desktop Search Points
data.userStatus.counters.pcSearch.forEach(x => totalEarnablePoints += (x.pointProgressMax - x.pointProgress))
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) {
@@ -153,12 +155,14 @@ export default class BrowserFunc {
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)
}
})
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) {

View File

@@ -12,6 +12,7 @@
"doDesktopSearch": true,
"doMobileSearch": true
},
"globalTimeout": 30000,
"searchSettings": {
"useGeoLocaleQueries": false,
"scrollRandomResults": true,
@@ -25,5 +26,6 @@
"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

@@ -2,6 +2,7 @@ import { Page } from 'playwright'
import readline from 'readline'
import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
const rl = readline.createInterface({
input: process.stdin,
@@ -26,14 +27,12 @@ export class Login {
if (!isLoggedIn) {
// Check if account is locked
const isLocked = await page.waitForSelector('.serviceAbusePageContainer', { state: 'visible', timeout: 10_000 }).then(() => true).catch(() => false)
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', { state: 'visible', timeout: 10_000 })
await this.execLogin(page, email, password)
this.bot.log('LOGIN', 'Logged into Microsoft successfully')
} else {
@@ -43,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')
@@ -54,51 +56,110 @@ export class Login {
private async execLogin(page: Page, email: string, password: string) {
try {
// Enter email
await page.fill('#i0116', email)
await page.click('#idSIButton9')
this.bot.log('LOGIN', 'Email entered successfully')
try {
// Enter password
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 })
await this.bot.utils.wait(2000)
await page.fill('#i0118', password)
await page.click('#idSIButton9')
// When erroring at this stage it means a 2FA code is required
} catch (error) {
this.bot.log('LOGIN', '2FA code required')
// Wait for user input
const code = await new Promise<string>((resolve) => {
rl.question('Enter 2FA code:\n', (input) => {
rl.close()
resolve(input)
})
})
await page.fill('input[name="otc"]', code)
await page.keyboard.press('Enter')
}
this.bot.log('LOGIN', 'Password entered successfully')
} catch (error) {
this.bot.log('LOGIN', 'An error occurred:' + error, 'error')
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 !== 'rewards.bing.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="RewardsPortal"]', { timeout: 10_000 })
this.bot.log('LOGIN', 'Successfully logged into the rewards portal')
}
private async checkBingLogin(page: Page): Promise<void> {

View File

@@ -87,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')
@@ -132,13 +132,13 @@ export class Workers {
}
let selector = `[data-bi-id="${activity.offerId}"]`
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}"]`
selector = `[data-bi-id^="${activity.name}"]`
}
// Click element, it will be opened in a new tab
@@ -149,7 +149,7 @@ export class Workers {
// 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,
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(() => { })

View File

@@ -42,6 +42,8 @@ export class Search extends Workers {
// 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
@@ -98,7 +100,7 @@ export class Search extends Workers {
for (const term of relatedTerms.slice(1, 3)) {
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`)
searchCounters = await this.bingSearch(page, query.topic)
searchCounters = await this.bingSearch(page, term)
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
@@ -140,6 +142,8 @@ export class Search extends Workers {
window.scrollTo(0, 0)
})
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
@@ -332,4 +336,4 @@ export class Search extends Workers {
return missingPoints
}
}
}

View File

@@ -178,11 +178,11 @@ export class MicrosoftRewardsBot {
}
// Save cookies
const cookies = await browser.cookies()
await saveSessionData(this.config.sessionPath, account.email, this.isMobile, cookies)
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
// Close desktop browser
return await this.closeBrowser(browser, account.email)
await this.closeBrowser(browser, account.email)
return
}
// Mobile
@@ -242,13 +242,13 @@ export class MicrosoftRewardsBot {
log('MAIN-POINTS', `The script collected ${this.collectedPoints} points today`)
// Close mobile browser
return await this.closeBrowser(browser, account.email)
await this.closeBrowser(browser, account.email)
return
}
private async closeBrowser(browser: BrowserContext, email: string) {
// Save cookies
const cookies = await browser.cookies()
await saveSessionData(this.config.sessionPath, email, this.isMobile, cookies)
await saveSessionData(this.config.sessionPath, browser, email, this.isMobile)
// Close browser
await browser.close()

View File

@@ -5,6 +5,7 @@ export interface Config {
runOnZeroPoints: boolean;
clusters: number;
workers: Workers;
globalTimeout: number;
searchSettings: SearchSettings;
webhook: Webhook;
saveFingerprint: boolean;

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,4 +1,4 @@
import { Cookie } from 'playwright'
import { BrowserContext, Cookie } from 'playwright'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import fs from 'fs'
import path from 'path'
@@ -67,8 +67,10 @@ export async function loadSessionData(sessionPath: string, email: string, isMobi
}
}
export async function saveSessionData(sessionPath: string, email: string, isMobile: boolean, cookies: Cookie[]): Promise<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)