5 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
12 changed files with 187 additions and 97 deletions

View File

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

View File

@@ -37,4 +37,4 @@ COPY src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
RUN touch /var/log/cron.log RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally # Define the command to run your application with cron optionally
CMD sh -c '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' 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,11 +16,14 @@ 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. - 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) ## ## 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 1. Download the source code
2. Make changes to your `accounts.json` 2. Make changes to your `accounts.json` and `config.json`
3. **Headless mode must be enabled when using Docker.** You can do this using the `HEADLESS=true` environmental variable in docker run or docker compose.yaml (see below). Environmental variables are always prioritized over the values in 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 will run scheduled. 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. 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. 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 ### Option 1: build and run with docker run
1. Build or re-build the container image with: `docker build -t microsoft-rewards-script-docker .` 1. Build or re-build the container image with: `docker build -t microsoft-rewards-script-docker .`
@@ -31,14 +34,12 @@ Under development, however mainly for personal use!
docker run --name netsky -d \ docker run --name netsky -d \
-e TZ=America/New_York \ -e TZ=America/New_York \
-e HEADLESS=true \ -e HEADLESS=true \
-e SEARCH_DELAY_MIN=10000 \ -e RUN_ON_START=true \
-e SEARCH_DELAY_MAX=20000 \
-e CLUSTERS=1 \
-e CRON_START_TIME="0 5,11 * * *" \ -e CRON_START_TIME="0 5,11 * * *" \
microsoft-rewards-script-docker microsoft-rewards-script-docker
``` ```
3. Optionally, change any environmental variables other than `HEADLESS`, which must stay `=true` 3. Optionally, customize your config by adding any other environmental variables from the table below.
4. You can view logs with `docker logs netsky`. 4. You can view logs with `docker logs netsky`.
@@ -46,9 +47,9 @@ Under development, however mainly for personal use!
1. A basic docker compose.yaml has been provided. 1. A basic docker compose.yaml has been provided.
2. Optionally, change any environmental variables other than `HEADLESS`, which must stay `=true` 2. Optionally, customize your config by adding any other environmental variables from the table below.
3. Build or rebuild and start the container using `docker compose up -d --build` 3. Build and start the container using `docker compose up -d`.
4. You can view logs with `docker logs netsky` 4. You can view logs with `docker logs netsky`
@@ -62,17 +63,17 @@ Under development, however mainly for personal use!
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | RUN_ON_ZERO_POINTS | | 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 | | 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 | | 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` | WORKERS_DO_DAILY_SET | | workers.doDailySet | Complete daily set items | `true` | DO_DAILY_SET |
| workers.doMorePromotions | Complete promotional items | `true` | WORKERS_DO_MORE_PROMOTIONS | | workers.doMorePromotions | Complete promotional items | `true` | DO_MORE_PROMOTIONS |
| workers.doPunchCards | Complete punchcards | `true` | WORKERS_DO_PUNCH_CARDS | | workers.doPunchCards | Complete punchcards | `true` | DO_PUNCH_CARDS |
| workers.doDesktopSearch | Complete daily desktop searches | `true` | WORKERS_DO_DESKTOP_SEARCH | | workers.doDesktopSearch | Complete daily desktop searches | `true` | DO_DESKTOP_SEARCH |
| workers.doMobileSearch | Complete daily mobile searches | `true` | WORKERS_DO_MOBILE_SEARCH | | workers.doMobileSearch | Complete daily mobile searches | `true` | DO_MOBILE_SEARCH |
| globalTimeout | The length before the action gets timeout | `30000` (30 seconds) | GLOBAL_TIMEOUT | | 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) | SEARCH_SETTINGS_USE_GEO_LOCALE_QUERIES | | 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` | SEARCH_SETTINGS_SCROLL_RANDOM_RESULTS | | scrollRandomResults | Scroll randomly in search results | `true` | SCROLL_RANDOM_RESULTS |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | SEARCH_SETTINGS_CLICK_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.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` | SEARCH_SETTINGS_RETRY_MOBILE_SEARCH | | 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.enabled | Enable or disable your set webhook | `false` | WEBHOOK_ENABLED |
| webhook.url | Your Discord webhook URL | `null` | WEBHOOK_URL="" | | 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="" | | cronStartTime | Scheduled script run-time, *only available for docker implementation* | `0 5,11 * * *` (5:00 am, 11:00 am daily) | CRON_START_TIME="" |
@@ -82,6 +83,7 @@ Under development, however mainly for personal use!
- [x] Multi-Account Support - [x] Multi-Account Support
- [x] Session Storing - [x] Session Storing
- [x] 2FA Support - [x] 2FA Support
- [x] Passwordless Support
- [x] Headless Support - [x] Headless Support
- [x] Discord Webhook Support - [x] Discord Webhook Support
- [x] Desktop Searches - [x] Desktop Searches

View File

@@ -6,29 +6,8 @@ services:
- TZ=America/Toronto #change to your local timezone - TZ=America/Toronto #change to your local timezone
- NODE_ENV=production - NODE_ENV=production
- HEADLESS=true #do not change - HEADLESS=true #do not change
### the following are optional, you only need to include them if you want to enter a custom value, removing them will use the default values ### Customize your run schedule, default 5:00 am and 11:00 am, use crontab.guru for formatting
- BASE_URL=https://rewards.bing.com
- SESSION_PATH=sessions
- RUN_ON_ZERO_POINTS=false
- CLUSTERS=1
- SAVE_FINGERPRINT=false
- WORKERS_DO_DAILY_SET=true
- WORKERS_DO_MORE_PROMOTIONS=true
- WORKERS_DO_PUNCH_CARDS=true
- WORKERS_DO_DESKTOP_SEARCH=true
- WORKERS_DO_MOBILE_SEARCH=true
- SEARCH_SETTINGS_USE_GEO_LOCALE_QUERIES=false
- SEARCH_SETTINGS_SCROLL_RANDOM_RESULTS=true
- SEARCH_SETTINGS_CLICK_RANDOM_RESULTS=true
- SEARCH_SETTINGS_SEARCH_DELAY_MIN=10000 # Set the search delay longer, e.g. MIN=180000 and MAX=270000 if you live in a region where MS enforces a search cooldown
- SEARCH_SETTINGS_SEARCH_DELAY_MAX=20000
- SEARCH_SETTINGS_RETRY_MOBILE_SEARCH=true
- WEBHOOK_ENABLED=false
- WEBHOOK_URL=
### Customize your run schedule, default 5:00 am and 11:00 am, use crontab.guru if you're not sure
- CRON_START_TIME=0 5,11 * * * - CRON_START_TIME=0 5,11 * * *
### Run on start, set as false to only run the script per the cron schedule ### Run on start, set to false to only run the script per the cron schedule
- RUN_ON_START=true - RUN_ON_START=true
restart: unless-stopped restart: unless-stopped
volumes:
- .:/usr/src/microsoft-rewards-script

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "1.4.7", "version": "1.4.8",
"description": "Automatically do tasks for Microsoft Rewards but in TS!", "description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@@ -26,17 +26,17 @@
"author": "Netsky", "author": "Netsky",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.11.0", "@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
"typescript": "^5.4.5" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.2", "axios": "^1.7.4",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.51", "fingerprint-generator": "^2.1.54",
"fingerprint-injector": "^2.1.51", "fingerprint-injector": "^2.1.54",
"playwright": "^1.44.1", "playwright": "^1.46.1",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
} }
} }

View File

@@ -158,7 +158,7 @@ export default class BrowserFunc {
if (data.morePromotions?.length) { if (data.morePromotions?.length) {
data.morePromotions.forEach(x => { data.morePromotions.forEach(x => {
// Only count points from supported activities // Only count points from supported activities
if (['quiz', 'urlreward'].includes(x.promotionType)) { if (['quiz', 'urlreward'].includes(x.promotionType) && !x.attributes.is_unlocked) {
totalEarnablePoints += (x.pointProgressMax - x.pointProgress) totalEarnablePoints += (x.pointProgressMax - x.pointProgress)
} }
}) })

View File

@@ -1 +1 @@
${CRON_START_TIME} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /var/log/cron.log 2>&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

@@ -56,51 +56,110 @@ export class Login {
private async execLogin(page: Page, email: string, password: string) { private async execLogin(page: Page, email: string, password: string) {
try { try {
// Enter email await this.enterEmail(page, email)
await page.fill('#i0116', email) await this.enterPassword(page, password)
await page.click('#idSIButton9') await this.checkLoggedIn(page)
} catch (error: any) {
this.bot.log('LOGIN', 'Email entered successfully') this.bot.log('LOGIN', 'An error occurred: ' + error.message, 'error')
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')
} }
}
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) 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 // Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { 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> { private async checkBingLogin(page: Page): Promise<void> {

View File

@@ -87,7 +87,7 @@ export class Workers {
morePromotions.push(data.promotionalItem as unknown as MorePromotion) 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) { if (!activitiesUncompleted.length) {
this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') 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) { if (punchCard) {
selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity) selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity)
} else if (activity.name.toLowerCase().includes('membercenter')) { } 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 // Click element, it will be opened in a new tab

View File

@@ -42,6 +42,8 @@ export class Search extends Workers {
// Mobile search doesn't seem to like related queries? // 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) }) 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 // Loop over Google search queries
for (let i = 0; i < queries.length; i++) { for (let i = 0; i < queries.length; i++) {
const query = queries[i] as string const query = queries[i] as string
@@ -98,7 +100,7 @@ export class Search extends Workers {
for (const term of relatedTerms.slice(1, 3)) { for (const term of relatedTerms.slice(1, 3)) {
this.bot.log('SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term} | Mobile: ${this.bot.isMobile}`) 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) const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before // If the new point amount is the same as before

View File

@@ -3,6 +3,9 @@
# Set up environment variables # Set up environment variables
export PATH=$PATH:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin 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 # Change directory to the application directory
cd /usr/src/microsoft-rewards-script cd /usr/src/microsoft-rewards-script

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);
}