mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 01:36:16 +00:00
New structure
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import Browser from '../browser/Browser'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { log } from '../util/Logger'
|
||||
import { log } from '../util/notifications/Logger'
|
||||
import { AccountCreator } from './AccountCreator'
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -9,11 +9,11 @@ async function main(): Promise<void> {
|
||||
let referralUrl: string | undefined
|
||||
let recoveryEmail: string | undefined
|
||||
let autoAccept = false
|
||||
|
||||
|
||||
// Parse arguments - ULTRA SIMPLE
|
||||
for (const arg of args) {
|
||||
if (!arg) continue
|
||||
|
||||
|
||||
if (arg === '-y' || arg === '--yes' || arg === 'y' || arg === 'Y') {
|
||||
autoAccept = true
|
||||
} else if (arg.startsWith('http')) {
|
||||
@@ -23,7 +23,7 @@ async function main(): Promise<void> {
|
||||
recoveryEmail = arg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Banner
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
|
||||
@@ -34,18 +34,18 @@ async function main(): Promise<void> {
|
||||
log(false, 'CREATOR-CLI', ' Only interact when explicitly asked (e.g., CAPTCHA solving).', 'warn', 'yellow')
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
|
||||
// Display detected arguments
|
||||
if (referralUrl) {
|
||||
log(false, 'CREATOR-CLI', `✅ Referral URL: ${referralUrl}`, 'log', 'green')
|
||||
} else {
|
||||
log(false, 'CREATOR-CLI', '⚠️ No referral URL - account will NOT be linked to rewards', 'warn', 'yellow')
|
||||
}
|
||||
|
||||
|
||||
if (recoveryEmail) {
|
||||
log(false, 'CREATOR-CLI', `✅ Recovery email: ${recoveryEmail}`, 'log', 'green')
|
||||
}
|
||||
|
||||
|
||||
if (autoAccept) {
|
||||
log(false, 'CREATOR-CLI', '⚡ Auto-accept mode ENABLED (-y flag detected)', 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '🤖 All prompts will be auto-accepted', 'log', 'cyan')
|
||||
@@ -53,17 +53,17 @@ async function main(): Promise<void> {
|
||||
log(false, 'CREATOR-CLI', '🤖 Interactive mode: you will be asked for options', 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', '💡 Tip: Use -y flag to auto-accept all prompts', 'log', 'gray')
|
||||
}
|
||||
|
||||
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
|
||||
// Create a temporary bot instance to access browser creation
|
||||
const bot = new MicrosoftRewardsBot(false)
|
||||
const browserFactory = new Browser(bot)
|
||||
|
||||
|
||||
try {
|
||||
// Create browser (non-headless for user interaction with CAPTCHA)
|
||||
log(false, 'CREATOR-CLI', 'Opening browser (required for CAPTCHA solving)...', 'log')
|
||||
|
||||
|
||||
// Create empty proxy config (no proxy for account creation)
|
||||
const emptyProxy = {
|
||||
proxyAxios: false,
|
||||
@@ -72,44 +72,44 @@ async function main(): Promise<void> {
|
||||
password: '',
|
||||
username: ''
|
||||
}
|
||||
|
||||
|
||||
const browserContext = await browserFactory.createBrowser(emptyProxy, 'account-creator')
|
||||
|
||||
|
||||
log(false, 'CREATOR-CLI', '✅ Browser opened successfully', 'log', 'green')
|
||||
|
||||
|
||||
// Create account
|
||||
const creator = new AccountCreator(referralUrl, recoveryEmail, autoAccept)
|
||||
const result = await creator.create(browserContext)
|
||||
|
||||
|
||||
if (result) {
|
||||
// Success banner
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '✅ ACCOUNT CREATED SUCCESSFULLY!', 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
|
||||
|
||||
|
||||
// Display account details
|
||||
log(false, 'CREATOR-CLI', `📧 Email: ${result.email}`, 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', `🔐 Password: ${result.password}`, 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', `👤 Name: ${result.firstName} ${result.lastName}`, 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', `🎂 Birthdate: ${result.birthdate.day}/${result.birthdate.month}/${result.birthdate.year}`, 'log', 'cyan')
|
||||
|
||||
|
||||
if (result.referralUrl) {
|
||||
log(false, 'CREATOR-CLI', '🔗 Referral: Linked', 'log', 'green')
|
||||
}
|
||||
|
||||
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '💾 Account details saved to accounts-created/ directory', 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
|
||||
// Keep browser open - don't close
|
||||
log(false, 'CREATOR-CLI', '✅ Account creation complete! Browser will remain open.', 'log', 'green')
|
||||
log(false, 'CREATOR-CLI', 'You can now use the account or close the browser manually.', 'log', 'cyan')
|
||||
log(false, 'CREATOR-CLI', 'Press Ctrl+C to exit the script.', 'log', 'yellow')
|
||||
|
||||
|
||||
// Keep process alive indefinitely
|
||||
await new Promise(() => {}) // Never resolves
|
||||
await new Promise(() => { }) // Never resolves
|
||||
} else {
|
||||
// Failure
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
@@ -117,11 +117,11 @@ async function main(): Promise<void> {
|
||||
log(false, 'CREATOR-CLI', '❌ ACCOUNT CREATION FAILED', 'error')
|
||||
log(false, 'CREATOR-CLI', '='.repeat(60), 'error')
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
|
||||
await browserContext.close()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
log(false, 'CREATOR-CLI', '', 'log') // Empty line
|
||||
|
||||
@@ -4,8 +4,8 @@ import playwright, { BrowserContext } from 'rebrowser-playwright'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||
import { updateFingerprintUserAgent } from '../util/UserAgent'
|
||||
import { updateFingerprintUserAgent } from '../util/browser/UserAgent'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/state/Load'
|
||||
|
||||
class Browser {
|
||||
private bot: MicrosoftRewardsBot
|
||||
@@ -22,7 +22,7 @@ class Browser {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Auto-installing Chromium...', 'log')
|
||||
execSync('npx playwright install chromium', { stdio: 'ignore', timeout: 120000 })
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium installed successfully', 'log')
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
// FIXED: Improved error logging (no longer silent)
|
||||
const errorMsg = e instanceof Error ? e.message : String(e)
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Auto-install failed: ${errorMsg}`, 'warn')
|
||||
@@ -33,13 +33,13 @@ class Browser {
|
||||
try {
|
||||
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
|
||||
const headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false)
|
||||
|
||||
|
||||
const engineName = 'chromium'
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`)
|
||||
const proxyConfig = this.buildPlaywrightProxy(proxy)
|
||||
|
||||
const isLinux = process.platform === 'linux'
|
||||
|
||||
|
||||
// Base arguments for stability
|
||||
const baseArgs = [
|
||||
'--no-sandbox',
|
||||
@@ -49,7 +49,7 @@ class Browser {
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors'
|
||||
]
|
||||
|
||||
|
||||
// Linux stability fixes
|
||||
const linuxStabilityArgs = isLinux ? [
|
||||
'--disable-dev-shm-usage',
|
||||
@@ -88,10 +88,10 @@ class Browser {
|
||||
try {
|
||||
context.on('page', async (page) => {
|
||||
try {
|
||||
const viewport = this.bot.isMobile
|
||||
const viewport = this.bot.isMobile
|
||||
? { width: 390, height: 844 }
|
||||
: { width: 1280, height: 800 }
|
||||
|
||||
|
||||
await page.setViewportSize(viewport)
|
||||
|
||||
// Standard styling
|
||||
@@ -106,13 +106,13 @@ class Browser {
|
||||
}
|
||||
`
|
||||
document.documentElement.appendChild(style)
|
||||
} catch {/* ignore */}
|
||||
} catch {/* ignore */ }
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Context event handler warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AppUserData } from '../interface/AppUserData'
|
||||
import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
|
||||
import { EarnablePoints } from '../interface/Points'
|
||||
import { QuizData } from '../interface/QuizData'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { saveSessionData } from '../util/state/Load'
|
||||
|
||||
|
||||
export default class BrowserFunc {
|
||||
@@ -29,12 +29,12 @@ export default class BrowserFunc {
|
||||
const suspendedByHeader = await page.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
|
||||
if (suspendedByHeader) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error')
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// Secondary check: look for suspension text in main content area only
|
||||
try {
|
||||
const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || ''
|
||||
@@ -43,7 +43,7 @@ export default class BrowserFunc {
|
||||
/suspended\s+due\s+to\s+unusual\s+activity/i,
|
||||
/your\s+account\s+is\s+temporarily\s+suspended/i
|
||||
]
|
||||
|
||||
|
||||
const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent))
|
||||
if (isSuspended) {
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
|
||||
@@ -54,7 +54,7 @@ export default class BrowserFunc {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class BrowserFunc {
|
||||
if (isSuspended) {
|
||||
throw new Error('Account has been suspended!')
|
||||
}
|
||||
|
||||
|
||||
// Not suspended, just activities not loaded yet - continue to next iteration
|
||||
this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn')
|
||||
}
|
||||
@@ -133,10 +133,10 @@ export default class BrowserFunc {
|
||||
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
|
||||
await this.goHome(target)
|
||||
}
|
||||
|
||||
|
||||
// Reload with retry
|
||||
await this.reloadPageWithRetry(target, 2)
|
||||
|
||||
|
||||
// Wait for the more-activities element to ensure page is fully loaded
|
||||
await target.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((error) => {
|
||||
// Continuing is intentional: page may still be functional even if this specific element is missing
|
||||
@@ -149,7 +149,7 @@ export default class BrowserFunc {
|
||||
|
||||
if (!scriptContent) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard script not found on first try, attempting recovery', 'warn')
|
||||
|
||||
|
||||
// Force a navigation retry once before failing hard
|
||||
await this.goHome(target)
|
||||
await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch((error) => {
|
||||
@@ -157,9 +157,9 @@ export default class BrowserFunc {
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER-FUNC', `Dashboard recovery load failed: ${errorMsg}`, 'warn')
|
||||
})
|
||||
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
|
||||
|
||||
|
||||
scriptContent = await this.extractDashboardScript(target)
|
||||
|
||||
|
||||
if (!scriptContent) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
||||
throw new Error('Dashboard data not found within script - check page structure')
|
||||
@@ -192,14 +192,14 @@ export default class BrowserFunc {
|
||||
const startTime = Date.now()
|
||||
const MAX_TOTAL_TIME_MS = 30000 // 30 seconds max total
|
||||
let lastError: unknown = null
|
||||
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
// Check global timeout
|
||||
if (Date.now() - startTime > MAX_TOTAL_TIME_MS) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', `Reload retry exceeded total timeout (${MAX_TOTAL_TIME_MS}ms)`, 'warn')
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM)
|
||||
@@ -212,7 +212,7 @@ export default class BrowserFunc {
|
||||
if (msg.includes('has been closed')) {
|
||||
if (attempt === 1) {
|
||||
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
|
||||
try { await this.goHome(page) } catch {/* ignore */}
|
||||
try { await this.goHome(page) } catch {/* ignore */ }
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export default class BrowserFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (lastError) throw lastError
|
||||
}
|
||||
|
||||
@@ -233,12 +233,12 @@ export default class BrowserFunc {
|
||||
return await page.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const dashboardPatterns = ['var dashboard', 'dashboard=', 'dashboard :']
|
||||
|
||||
|
||||
const targetScript = scripts.find(script => {
|
||||
const text = script.innerText
|
||||
return text && dashboardPatterns.some(pattern => text.includes(pattern))
|
||||
})
|
||||
|
||||
|
||||
return targetScript?.innerText || null
|
||||
})
|
||||
}
|
||||
@@ -265,19 +265,19 @@ export default class BrowserFunc {
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
|
||||
|
||||
// Enhanced validation: check structure and type
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Validate essential dashboard properties exist
|
||||
if (!parsed.userStatus || typeof parsed.userStatus !== 'object') {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Successfully validated dashboard structure
|
||||
return parsed
|
||||
} catch (e) {
|
||||
@@ -401,7 +401,7 @@ export default class BrowserFunc {
|
||||
const checkInDay = parseInt(item.attributes.progress ?? '', 10) % 7
|
||||
const today = new Date()
|
||||
const lastUpdated = new Date(item.attributes.last_updated ?? '')
|
||||
|
||||
|
||||
if (checkInDay < 6 && today.getDate() !== lastUpdated.getDate()) {
|
||||
points.checkIn = parseInt(item.attributes['day_' + (checkInDay + 1) + '_points'] ?? '', 10)
|
||||
}
|
||||
@@ -493,10 +493,10 @@ export default class BrowserFunc {
|
||||
.map(el => $(el).text())
|
||||
.filter(t => t.length > 0)
|
||||
.map(t => t.substring(0, 100))
|
||||
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Script not found. Tried variables: ${possibleVariables.join(', ')}`, 'error')
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', `Found ${allScripts.length} scripts on page`, 'warn')
|
||||
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'GET-QUIZ-DATA', 'Script containing quiz data not found', 'error')
|
||||
throw new Error('Script containing quiz data not found - check page structure')
|
||||
}
|
||||
@@ -545,10 +545,10 @@ export default class BrowserFunc {
|
||||
const html = await page.content()
|
||||
const $ = load(html)
|
||||
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
const element = $('.offer-cta').toArray().find((x: unknown) => {
|
||||
const el = x as { attribs?: { href?: string } }
|
||||
return !!el.attribs?.href?.includes(activity.offerId)
|
||||
})
|
||||
if (element) {
|
||||
selector = `a[href*="${element.attribs.href}"]`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { load } from 'cheerio'
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { logError } from '../util/Logger'
|
||||
import { logError } from '../util/notifications/Logger'
|
||||
|
||||
type DismissButton = { selector: string; label: string; isXPath?: boolean }
|
||||
|
||||
@@ -145,14 +145,14 @@ export default class BrowserUtil {
|
||||
private async dismissTermsUpdateDialog(page: Page): Promise<number> {
|
||||
try {
|
||||
const { titleId, titleText, nextButton } = BrowserUtil.TERMS_UPDATE_SELECTORS
|
||||
|
||||
|
||||
// Check if terms update page is present
|
||||
const titleById = page.locator(titleId)
|
||||
const titleByText = page.locator('h1').filter({ hasText: titleText })
|
||||
|
||||
|
||||
const hasTitle = await titleById.isVisible({ timeout: 200 }).catch(() => false) ||
|
||||
await titleByText.first().isVisible({ timeout: 200 }).catch(() => false)
|
||||
|
||||
await titleByText.first().isVisible({ timeout: 200 }).catch(() => false)
|
||||
|
||||
if (!hasTitle) return 0
|
||||
|
||||
// Click the Next button
|
||||
@@ -199,9 +199,9 @@ export default class BrowserUtil {
|
||||
const $ = load(html)
|
||||
|
||||
const isNetworkError = $('body.neterror').length
|
||||
const hasHttp400Error = html.includes('HTTP ERROR 400') ||
|
||||
html.includes('This page isn\'t working') ||
|
||||
html.includes('This page is not working')
|
||||
const hasHttp400Error = html.includes('HTTP ERROR 400') ||
|
||||
html.includes('This page isn\'t working') ||
|
||||
html.includes('This page is not working')
|
||||
|
||||
if (isNetworkError || hasHttp400Error) {
|
||||
const errorType = hasHttp400Error ? 'HTTP 400' : 'network error'
|
||||
|
||||
@@ -18,7 +18,7 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
|
||||
const parsed = Number(raw)
|
||||
if (!Number.isFinite(parsed)) {
|
||||
queueMicrotask(() => {
|
||||
import('./util/Logger').then(({ log }) => {
|
||||
import('./util/notifications/Logger').then(({ log }) => {
|
||||
log('main', 'CONSTANTS', `Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}`, 'warn')
|
||||
}).catch(() => {
|
||||
process.stderr.write(`[Constants] Invalid ${key}="${raw}" (not a finite number), using default: ${defaultValue}\n`)
|
||||
@@ -29,7 +29,7 @@ function parseEnvNumber(key: string, defaultValue: number, min: number, max: num
|
||||
|
||||
if (parsed < min || parsed > max) {
|
||||
queueMicrotask(() => {
|
||||
import('./util/Logger').then(({ log }) => {
|
||||
import('./util/notifications/Logger').then(({ log }) => {
|
||||
log('main', 'CONSTANTS', `${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}`, 'warn')
|
||||
}).catch(() => {
|
||||
process.stderr.write(`[Constants] ${key}=${parsed} out of range [${min}, ${max}], using default: ${defaultValue}\n`)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs
|
||||
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-bot/src/run_daily.sh >> /proc/1/fd/1 2>&1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import { log as botLog } from '../util/Logger'
|
||||
import { getErrorMessage } from '../util/Utils'
|
||||
import { getErrorMessage } from '../util/core/Utils'
|
||||
import { log as botLog } from '../util/notifications/Logger'
|
||||
import { dashboardState } from './state'
|
||||
|
||||
export class BotController {
|
||||
@@ -14,7 +14,7 @@ export class BotController {
|
||||
|
||||
private log(message: string, level: 'log' | 'warn' | 'error' = 'log'): void {
|
||||
botLog('main', 'BOT-CONTROLLER', message, level)
|
||||
|
||||
|
||||
dashboardState.addLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
@@ -29,7 +29,7 @@ export class BotController {
|
||||
if (this.botInstance) {
|
||||
return { success: false, error: 'Bot is already running' }
|
||||
}
|
||||
|
||||
|
||||
if (this.isStarting) {
|
||||
return { success: false, error: 'Bot is currently starting, please wait' }
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class BotController {
|
||||
this.log('🚀 Starting bot...', 'log')
|
||||
|
||||
const { MicrosoftRewardsBot } = await import('../index')
|
||||
|
||||
|
||||
this.botInstance = new MicrosoftRewardsBot(false)
|
||||
this.startTime = new Date()
|
||||
dashboardState.setRunning(true)
|
||||
@@ -49,10 +49,10 @@ export class BotController {
|
||||
void (async () => {
|
||||
try {
|
||||
this.log('✓ Bot initialized, starting execution...', 'log')
|
||||
|
||||
|
||||
await this.botInstance!.initialize()
|
||||
await this.botInstance!.run()
|
||||
|
||||
|
||||
this.log('✓ Bot completed successfully', 'log')
|
||||
} catch (error) {
|
||||
this.log(`Bot error: ${getErrorMessage(error)}`, 'error')
|
||||
@@ -81,7 +81,7 @@ export class BotController {
|
||||
try {
|
||||
this.log('🛑 Stopping bot...', 'warn')
|
||||
this.log('⚠ Note: Bot will complete current task before stopping', 'warn')
|
||||
|
||||
|
||||
this.cleanup()
|
||||
return { success: true }
|
||||
|
||||
@@ -95,14 +95,14 @@ export class BotController {
|
||||
|
||||
public async restart(): Promise<{ success: boolean; error?: string; pid?: number }> {
|
||||
this.log('🔄 Restarting bot...', 'log')
|
||||
|
||||
|
||||
const stopResult = this.stop()
|
||||
if (!stopResult.success && stopResult.error !== 'Bot is not running') {
|
||||
return { success: false, error: `Failed to stop: ${stopResult.error}` }
|
||||
}
|
||||
|
||||
|
||||
await this.wait(2000)
|
||||
|
||||
|
||||
return await this.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getConfigPath, loadAccounts, loadConfig } from '../util/Load'
|
||||
import { getConfigPath, loadAccounts, loadConfig } from '../util/state/Load'
|
||||
import { botController } from './BotController'
|
||||
import { dashboardState } from './state'
|
||||
|
||||
@@ -100,7 +100,7 @@ apiRouter.get('/config', (_req: Request, res: Response) => {
|
||||
try {
|
||||
const config = loadConfig()
|
||||
const safe = JSON.parse(JSON.stringify(config))
|
||||
|
||||
|
||||
// Mask sensitive data
|
||||
if (safe.webhook?.url) safe.webhook.url = maskUrl(safe.webhook.url)
|
||||
if (safe.conclusionWebhook?.url) safe.conclusionWebhook.url = maskUrl(safe.conclusionWebhook.url)
|
||||
@@ -117,7 +117,7 @@ apiRouter.post('/config', (req: Request, res: Response): void => {
|
||||
try {
|
||||
const newConfig = req.body
|
||||
const configPath = getConfigPath()
|
||||
|
||||
|
||||
if (!configPath || !fs.existsSync(configPath)) {
|
||||
res.status(404).json({ error: 'Config file not found' })
|
||||
return
|
||||
@@ -146,7 +146,7 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> =>
|
||||
}
|
||||
|
||||
const result = await botController.start()
|
||||
|
||||
|
||||
if (result.success) {
|
||||
sendSuccess(res, { message: 'Bot started successfully', pid: result.pid })
|
||||
} else {
|
||||
@@ -161,7 +161,7 @@ apiRouter.post('/start', async (_req: Request, res: Response): Promise<void> =>
|
||||
apiRouter.post('/stop', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const result = botController.stop()
|
||||
|
||||
|
||||
if (result.success) {
|
||||
sendSuccess(res, { message: 'Bot stopped successfully' })
|
||||
} else {
|
||||
@@ -176,7 +176,7 @@ apiRouter.post('/stop', (_req: Request, res: Response): void => {
|
||||
apiRouter.post('/restart', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await botController.restart()
|
||||
|
||||
|
||||
if (result.success) {
|
||||
sendSuccess(res, { message: 'Bot restarted successfully', pid: result.pid })
|
||||
} else {
|
||||
@@ -194,7 +194,7 @@ apiRouter.get('/metrics', (_req: Request, res: Response) => {
|
||||
const totalPoints = accounts.reduce((sum, a) => sum + (a.points || 0), 0)
|
||||
const accountsWithErrors = accounts.filter(a => a.errors && a.errors.length > 0).length
|
||||
const avgPoints = accounts.length > 0 ? Math.round(totalPoints / accounts.length) : 0
|
||||
|
||||
|
||||
res.json({
|
||||
totalAccounts: accounts.length,
|
||||
totalPoints,
|
||||
@@ -218,14 +218,14 @@ apiRouter.get('/account/:email', (req: Request, res: Response): void => {
|
||||
res.status(400).json({ error: 'Email parameter required' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const account = dashboardState.getAccount(email)
|
||||
|
||||
|
||||
if (!account) {
|
||||
res.status(404).json({ error: 'Account not found' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
res.json(account)
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: getErr(error) })
|
||||
@@ -240,19 +240,19 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
|
||||
res.status(400).json({ error: 'Email parameter required' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const account = dashboardState.getAccount(email)
|
||||
|
||||
|
||||
if (!account) {
|
||||
res.status(404).json({ error: 'Account not found' })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
dashboardState.updateAccount(email, {
|
||||
status: 'idle',
|
||||
errors: []
|
||||
})
|
||||
|
||||
|
||||
res.json({ success: true })
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: getErr(error) })
|
||||
@@ -263,10 +263,10 @@ apiRouter.post('/account/:email/reset', (req: Request, res: Response): void => {
|
||||
function maskUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
const maskedHost = parsed.hostname.length > 6
|
||||
const maskedHost = parsed.hostname.length > 6
|
||||
? `${parsed.hostname.slice(0, 3)}***${parsed.hostname.slice(-3)}`
|
||||
: '***'
|
||||
const maskedPath = parsed.pathname.length > 5
|
||||
const maskedPath = parsed.pathname.length > 5
|
||||
? `${parsed.pathname.slice(0, 3)}***`
|
||||
: '***'
|
||||
return `${parsed.protocol}//${maskedHost}${maskedPath}`
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs'
|
||||
import { createServer } from 'http'
|
||||
import path from 'path'
|
||||
import { WebSocket, WebSocketServer } from 'ws'
|
||||
import { log as botLog } from '../util/Logger'
|
||||
import { log as botLog } from '../util/notifications/Logger'
|
||||
import { apiRouter } from './routes'
|
||||
import { DashboardLog, dashboardState } from './state'
|
||||
|
||||
@@ -41,7 +41,7 @@ export class DashboardServer {
|
||||
|
||||
private setupMiddleware(): void {
|
||||
this.app.use(express.json())
|
||||
|
||||
|
||||
// Disable caching for all static files
|
||||
this.app.use((req, res, next) => {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
|
||||
@@ -49,7 +49,7 @@ export class DashboardServer {
|
||||
res.set('Expires', '0')
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
this.app.use('/assets', express.static(path.join(__dirname, '../../assets'), {
|
||||
etag: false,
|
||||
maxAge: 0
|
||||
@@ -62,7 +62,7 @@ export class DashboardServer {
|
||||
|
||||
private setupRoutes(): void {
|
||||
this.app.use('/api', apiRouter)
|
||||
|
||||
|
||||
// Health check
|
||||
this.app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', uptime: process.uptime() })
|
||||
@@ -71,12 +71,12 @@ export class DashboardServer {
|
||||
// Serve dashboard UI
|
||||
this.app.get('/', (_req, res) => {
|
||||
const indexPath = path.join(__dirname, '../../public/index.html')
|
||||
|
||||
|
||||
// Force no cache on HTML files
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
|
||||
res.set('Pragma', 'no-cache')
|
||||
res.set('Expires', '0')
|
||||
|
||||
|
||||
if (fs.existsSync(indexPath)) {
|
||||
res.sendFile(indexPath)
|
||||
} else {
|
||||
@@ -117,9 +117,9 @@ export class DashboardServer {
|
||||
const recentLogs = dashboardState.getLogs(100)
|
||||
const status = dashboardState.getStatus()
|
||||
const accounts = dashboardState.getAccounts()
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'init',
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'init',
|
||||
data: {
|
||||
logs: recentLogs,
|
||||
status,
|
||||
@@ -135,7 +135,7 @@ export class DashboardServer {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const loggerModule = require('../util/Logger') as { log: typeof botLog }
|
||||
const originalLog = loggerModule.log
|
||||
|
||||
|
||||
loggerModule.log = (
|
||||
isMobile: boolean | 'main',
|
||||
title: string,
|
||||
@@ -145,7 +145,7 @@ export class DashboardServer {
|
||||
) => {
|
||||
// Call original log function
|
||||
const result = originalLog(isMobile, title, message, type, color as keyof typeof import('chalk'))
|
||||
|
||||
|
||||
// Create log entry for dashboard
|
||||
const logEntry: DashboardLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -154,14 +154,14 @@ export class DashboardServer {
|
||||
title,
|
||||
message
|
||||
}
|
||||
|
||||
|
||||
// Add to dashboard state and broadcast
|
||||
dashboardState.addLog(logEntry)
|
||||
this.broadcastUpdate('log', { log: logEntry })
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
dashLog('Bot log interception active')
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { Account } from '../interface/Account'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface DesktopFlowResult {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { saveSessionData } from '../util/state/Load'
|
||||
|
||||
/**
|
||||
* Handle compromised/security check mode for an account
|
||||
@@ -27,7 +27,7 @@ export async function handleCompromisedMode(
|
||||
isMobile: boolean
|
||||
): Promise<{ keepBrowserOpen: boolean }> {
|
||||
const flowContext = isMobile ? 'MOBILE-FLOW' : 'DESKTOP-FLOW'
|
||||
|
||||
|
||||
bot.log(
|
||||
isMobile,
|
||||
flowContext,
|
||||
@@ -35,10 +35,10 @@ export async function handleCompromisedMode(
|
||||
'warn',
|
||||
'yellow'
|
||||
)
|
||||
|
||||
|
||||
// Send security alert webhook
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('../util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
bot.config,
|
||||
isMobile ? '🔐 Security Check (Mobile)' : '🔐 Security Check',
|
||||
@@ -50,7 +50,7 @@ export async function handleCompromisedMode(
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
bot.log(isMobile, flowContext, `Failed to send security webhook: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
|
||||
// Save session for convenience (non-critical)
|
||||
try {
|
||||
await saveSessionData(bot.config.sessionPath, bot.homePage.context(), account, isMobile)
|
||||
@@ -58,6 +58,6 @@ export async function handleCompromisedMode(
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
bot.log(isMobile, flowContext, `Failed to save session: ${errorMsg}`, 'warn')
|
||||
}
|
||||
|
||||
|
||||
return { keepBrowserOpen: true }
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { Account } from '../interface/Account'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/BrowserFactory'
|
||||
import { MobileRetryTracker } from '../util/MobileRetryTracker'
|
||||
import { closeBrowserSafely, createBrowserInstance } from '../util/browser/BrowserFactory'
|
||||
import { MobileRetryTracker } from '../util/state/MobileRetryTracker'
|
||||
import { handleCompromisedMode } from './FlowUtils'
|
||||
|
||||
export interface MobileFlowResult {
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
*/
|
||||
|
||||
import type { Config } from '../interface/Config'
|
||||
import { ConclusionWebhook } from '../util/ConclusionWebhook'
|
||||
import { JobState } from '../util/JobState'
|
||||
import { log } from '../util/Logger'
|
||||
import { Ntfy } from '../util/Ntfy'
|
||||
import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
|
||||
import { log } from '../util/notifications/Logger'
|
||||
import { Ntfy } from '../util/notifications/Ntfy'
|
||||
import { JobState } from '../util/state/JobState'
|
||||
|
||||
export interface AccountResult {
|
||||
email: string
|
||||
@@ -54,7 +54,7 @@ export class SummaryReporter {
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
const seconds = duration % 60
|
||||
|
||||
const durationText = hours > 0
|
||||
const durationText = hours > 0
|
||||
? `${hours}h ${minutes}m ${seconds}s`
|
||||
: minutes > 0
|
||||
? `${minutes}m ${seconds}s`
|
||||
@@ -67,7 +67,7 @@ export class SummaryReporter {
|
||||
for (const account of summary.accounts) {
|
||||
const status = account.errors?.length ? '❌' : '✅'
|
||||
description += `${status} ${account.email}: ${account.pointsEarned} points (${Math.round(account.runDuration / 1000)}s)\n`
|
||||
|
||||
|
||||
if (account.errors?.length) {
|
||||
description += ` ⚠️ ${account.errors[0]}\n`
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export class SummaryReporter {
|
||||
|
||||
try {
|
||||
const message = `Collected ${summary.totalPoints} points across ${summary.accounts.length} account(s). Success: ${summary.successCount}, Failed: ${summary.failureCount}`
|
||||
|
||||
|
||||
await Ntfy(message, summary.failureCount > 0 ? 'warn' : 'log')
|
||||
} catch (error) {
|
||||
log('main', 'SUMMARY', `Failed to send Ntfy notification: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||
@@ -109,7 +109,7 @@ export class SummaryReporter {
|
||||
try {
|
||||
const day = summary.endTime.toISOString().split('T')?.[0]
|
||||
if (!day) return
|
||||
|
||||
|
||||
for (const account of summary.accounts) {
|
||||
this.jobState.markAccountComplete(
|
||||
account.email,
|
||||
@@ -133,12 +133,12 @@ export class SummaryReporter {
|
||||
log('main', 'SUMMARY', '═'.repeat(80))
|
||||
log('main', 'SUMMARY', '📊 EXECUTION SUMMARY')
|
||||
log('main', 'SUMMARY', '═'.repeat(80))
|
||||
|
||||
|
||||
const duration = Math.round((summary.endTime.getTime() - summary.startTime.getTime()) / 1000)
|
||||
log('main', 'SUMMARY', `⏱️ Duration: ${Math.floor(duration / 60)}m ${duration % 60}s`)
|
||||
log('main', 'SUMMARY', `📈 Total Points Collected: ${summary.totalPoints}`)
|
||||
log('main', 'SUMMARY', `✅ Successful Accounts: ${summary.successCount}/${summary.accounts.length}`)
|
||||
|
||||
|
||||
if (summary.failureCount > 0) {
|
||||
log('main', 'SUMMARY', `❌ Failed Accounts: ${summary.failureCount}`, 'warn')
|
||||
}
|
||||
@@ -150,10 +150,10 @@ export class SummaryReporter {
|
||||
for (const account of summary.accounts) {
|
||||
const status = account.errors?.length ? '❌ FAILED' : '✅ SUCCESS'
|
||||
const duration = Math.round(account.runDuration / 1000)
|
||||
|
||||
|
||||
log('main', 'SUMMARY', `${status} | ${account.email}`)
|
||||
log('main', 'SUMMARY', ` Points: ${account.pointsEarned} | Duration: ${duration}s`)
|
||||
|
||||
|
||||
if (account.errors?.length) {
|
||||
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,23 +4,23 @@ import { TIMEOUTS } from '../constants'
|
||||
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
|
||||
import JobState from '../util/JobState'
|
||||
import { logError } from '../util/Logger'
|
||||
import { Retry } from '../util/Retry'
|
||||
import { Retry } from '../util/core/Retry'
|
||||
import { AdaptiveThrottler } from '../util/notifications/AdaptiveThrottler'
|
||||
import { logError } from '../util/notifications/Logger'
|
||||
import JobState from '../util/state/JobState'
|
||||
|
||||
// Selector patterns (extracted to avoid magic strings)
|
||||
const ACTIVITY_SELECTORS = {
|
||||
byName: (name: string) => `[data-bi-id^="${name}"] .pointLink:not(.contentContainer .pointLink)`,
|
||||
byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .pointLink:not(.contentContainer .pointLink)`
|
||||
byName: (name: string) => `[data-bi-id^="${name}"] .pointLink:not(.contentContainer .pointLink)`,
|
||||
byOfferId: (offerId: string) => `[data-bi-id^="${offerId}"] .pointLink:not(.contentContainer .pointLink)`
|
||||
} as const
|
||||
|
||||
// Activity processing delays (in milliseconds)
|
||||
const ACTIVITY_DELAYS = {
|
||||
THROTTLE_MIN: 800,
|
||||
THROTTLE_MAX: 1400,
|
||||
ACTIVITY_SPACING_MIN: 1200,
|
||||
ACTIVITY_SPACING_MAX: 2600
|
||||
THROTTLE_MIN: 800,
|
||||
THROTTLE_MAX: 1400,
|
||||
ACTIVITY_SPACING_MIN: 1200,
|
||||
ACTIVITY_SPACING_MAX: 2600
|
||||
} as const
|
||||
|
||||
export class Workers {
|
||||
@@ -220,9 +220,9 @@ export class Workers {
|
||||
if (!activity.offerId) {
|
||||
// IMPROVED: More prominent logging for data integrity issue
|
||||
this.bot.log(
|
||||
this.bot.isMobile,
|
||||
'WORKERS',
|
||||
`⚠️ DATA INTEGRITY: Activity "${activity.name || activity.title}" is missing offerId field. This may indicate a dashboard API change or data corruption. Falling back to name-based selector.`,
|
||||
this.bot.isMobile,
|
||||
'WORKERS',
|
||||
`⚠️ DATA INTEGRITY: Activity "${activity.name || activity.title}" is missing offerId field. This may indicate a dashboard API change or data corruption. Falling back to name-based selector.`,
|
||||
'warn'
|
||||
)
|
||||
return ACTIVITY_SELECTORS.byName(activity.name)
|
||||
@@ -239,7 +239,7 @@ export class Workers {
|
||||
|
||||
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
|
||||
|
||||
|
||||
// Check if element exists before clicking (avoid 30s timeout)
|
||||
try {
|
||||
await page.waitForSelector(selector, { timeout: TIMEOUTS.NETWORK_IDLE })
|
||||
@@ -254,7 +254,7 @@ export class Workers {
|
||||
|
||||
// Execute activity with timeout protection using Promise.race
|
||||
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
|
||||
|
||||
|
||||
await retry.run(async () => {
|
||||
const activityPromise = this.bot.activities.run(page, activity)
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
@@ -264,7 +264,7 @@ export class Workers {
|
||||
// Clean up timer if activity completes first
|
||||
activityPromise.finally(() => clearTimeout(timer))
|
||||
})
|
||||
|
||||
|
||||
try {
|
||||
await Promise.race([activityPromise, timeoutPromise])
|
||||
throttle.record(true)
|
||||
|
||||
24
src/index.ts
24
src/index.ts
@@ -7,16 +7,16 @@ import type { Page } from 'playwright'
|
||||
import { createInterface } from 'readline'
|
||||
import BrowserFunc from './browser/BrowserFunc'
|
||||
import BrowserUtil from './browser/BrowserUtil'
|
||||
import Axios from './util/Axios'
|
||||
import { detectBanReason } from './util/BanDetector'
|
||||
import Humanizer from './util/Humanizer'
|
||||
import JobState from './util/JobState'
|
||||
import { loadAccounts, loadConfig } from './util/Load'
|
||||
import { log } from './util/Logger'
|
||||
import { MobileRetryTracker } from './util/MobileRetryTracker'
|
||||
import { QueryDiversityEngine } from './util/QueryDiversityEngine'
|
||||
import { StartupValidator } from './util/StartupValidator'
|
||||
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/Utils'
|
||||
import Humanizer from './util/browser/Humanizer'
|
||||
import { formatDetailedError, normalizeRecoveryEmail, shortErrorMessage, Util } from './util/core/Utils'
|
||||
import Axios from './util/network/Axios'
|
||||
import { QueryDiversityEngine } from './util/network/QueryDiversityEngine'
|
||||
import { log } from './util/notifications/Logger'
|
||||
import JobState from './util/state/JobState'
|
||||
import { loadAccounts, loadConfig } from './util/state/Load'
|
||||
import { MobileRetryTracker } from './util/state/MobileRetryTracker'
|
||||
import { detectBanReason } from './util/validation/BanDetector'
|
||||
import { StartupValidator } from './util/validation/StartupValidator'
|
||||
|
||||
import { Activities } from './functions/Activities'
|
||||
import { Login } from './functions/Login'
|
||||
@@ -629,7 +629,7 @@ export class MicrosoftRewardsBot {
|
||||
try {
|
||||
const h = this.config?.humanization
|
||||
if (!h || h.immediateBanAlert === false) return
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🚫 Ban Detected',
|
||||
@@ -806,7 +806,7 @@ export class MicrosoftRewardsBot {
|
||||
/** Send a strong alert to all channels and mention @everyone when entering global security standby. */
|
||||
private async sendGlobalSecurityStandbyAlert(email: string, reason: string): Promise<void> {
|
||||
try {
|
||||
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
|
||||
const { ConclusionWebhook } = await import('./util/notifications/ConclusionWebhook')
|
||||
await ConclusionWebhook(
|
||||
this.config,
|
||||
'🚨 Critical Security Alert',
|
||||
|
||||
156
src/run_daily.sh
156
src/run_daily.sh
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin"
|
||||
export PLAYWRIGHT_BROWSERS_PATH=0
|
||||
export TZ="${TZ:-UTC}"
|
||||
|
||||
cd /usr/src/microsoft-rewards-bot
|
||||
|
||||
LOCKFILE=/tmp/run_daily.lock
|
||||
|
||||
# -------------------------------
|
||||
# Function: Check and fix lockfile integrity
|
||||
# -------------------------------
|
||||
self_heal_lockfile() {
|
||||
# If lockfile exists but is empty → remove it
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
local lock_content
|
||||
lock_content=$(<"$LOCKFILE" || echo "")
|
||||
|
||||
if [[ -z "$lock_content" ]]; then
|
||||
echo "[$(date)] [run_daily.sh] Found empty lockfile → removing."
|
||||
rm -f "$LOCKFILE"
|
||||
return
|
||||
fi
|
||||
|
||||
# If lockfile contains non-numeric PID → remove it
|
||||
if ! [[ "$lock_content" =~ ^[0-9]+$ ]]; then
|
||||
echo "[$(date)] [run_daily.sh] Found corrupted lockfile content ('$lock_content') → removing."
|
||||
rm -f "$LOCKFILE"
|
||||
return
|
||||
fi
|
||||
|
||||
# If lockfile contains PID but process is dead → remove it
|
||||
if ! kill -0 "$lock_content" 2>/dev/null; then
|
||||
echo "[$(date)] [run_daily.sh] Lockfile PID $lock_content is dead → removing stale lock."
|
||||
rm -f "$LOCKFILE"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# Function: Acquire lock
|
||||
# -------------------------------
|
||||
acquire_lock() {
|
||||
local max_attempts=5
|
||||
local attempt=0
|
||||
local timeout_hours=${STUCK_PROCESS_TIMEOUT_HOURS:-8}
|
||||
local timeout_seconds=$((timeout_hours * 3600))
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
# Try to create lock with current PID
|
||||
if (set -C; echo "$$" > "$LOCKFILE") 2>/dev/null; then
|
||||
echo "[$(date)] [run_daily.sh] Lock acquired successfully (PID: $$)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Lock exists, validate it
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
local existing_pid
|
||||
existing_pid=$(<"$LOCKFILE" || echo "")
|
||||
|
||||
echo "[$(date)] [run_daily.sh] Lock file exists with PID: '$existing_pid'"
|
||||
|
||||
# If lockfile content is invalid → delete and retry
|
||||
if [[ -z "$existing_pid" || ! "$existing_pid" =~ ^[0-9]+$ ]]; then
|
||||
echo "[$(date)] [run_daily.sh] Removing invalid lockfile → retrying..."
|
||||
rm -f "$LOCKFILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
# If process is dead → delete and retry
|
||||
if ! kill -0 "$existing_pid" 2>/dev/null; then
|
||||
echo "[$(date)] [run_daily.sh] Removing stale lock (dead PID: $existing_pid)"
|
||||
rm -f "$LOCKFILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check process runtime → kill if exceeded timeout
|
||||
local process_age
|
||||
if process_age=$(ps -o etimes= -p "$existing_pid" 2>/dev/null | tr -d ' '); then
|
||||
if [ "$process_age" -gt "$timeout_seconds" ]; then
|
||||
echo "[$(date)] [run_daily.sh] Killing stuck process $existing_pid (${process_age}s > ${timeout_hours}h)"
|
||||
kill -TERM "$existing_pid" 2>/dev/null || true
|
||||
sleep 5
|
||||
kill -KILL "$existing_pid" 2>/dev/null || true
|
||||
rm -f "$LOCKFILE"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[$(date)] [run_daily.sh] Lock held by PID $existing_pid, attempt $((attempt + 1))/$max_attempts"
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
|
||||
echo "[$(date)] [run_daily.sh] Could not acquire lock after $max_attempts attempts; exiting."
|
||||
return 1
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# Function: Release lock
|
||||
# -------------------------------
|
||||
release_lock() {
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
local lock_pid
|
||||
lock_pid=$(<"$LOCKFILE")
|
||||
if [ "$lock_pid" = "$$" ]; then
|
||||
rm -f "$LOCKFILE"
|
||||
echo "[$(date)] [run_daily.sh] Lock released (PID: $$)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Always release lock on exit — but only if we acquired it
|
||||
trap 'release_lock' EXIT INT TERM
|
||||
|
||||
# -------------------------------
|
||||
# MAIN EXECUTION FLOW
|
||||
# -------------------------------
|
||||
echo "[$(date)] [run_daily.sh] Current process PID: $$"
|
||||
|
||||
# Self-heal any broken or empty locks before proceeding
|
||||
self_heal_lockfile
|
||||
|
||||
# Attempt to acquire the lock safely
|
||||
if ! acquire_lock; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Random sleep between MIN and MAX to spread execution
|
||||
MINWAIT=${MIN_SLEEP_MINUTES:-5}
|
||||
MAXWAIT=${MAX_SLEEP_MINUTES:-50}
|
||||
MINWAIT_SEC=$((MINWAIT*60))
|
||||
MAXWAIT_SEC=$((MAXWAIT*60))
|
||||
|
||||
if [ "${SKIP_RANDOM_SLEEP:-false}" != "true" ]; then
|
||||
SLEEPTIME=$(( MINWAIT_SEC + RANDOM % (MAXWAIT_SEC - MINWAIT_SEC) ))
|
||||
echo "[$(date)] [run_daily.sh] Sleeping for $((SLEEPTIME/60)) minutes ($SLEEPTIME seconds)"
|
||||
sleep "$SLEEPTIME"
|
||||
else
|
||||
echo "[$(date)] [run_daily.sh] Skipping random sleep"
|
||||
fi
|
||||
|
||||
# Start the actual script
|
||||
echo "[$(date)] [run_daily.sh] Starting script..."
|
||||
if npm start; then
|
||||
echo "[$(date)] [run_daily.sh] Script completed successfully."
|
||||
else
|
||||
echo "[$(date)] [run_daily.sh] ERROR: Script failed!" >&2
|
||||
fi
|
||||
|
||||
echo "[$(date)] [run_daily.sh] Script finished"
|
||||
# Lock is released automatically via trap
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContext } from 'rebrowser-playwright'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { AccountProxy } from '../interface/Account'
|
||||
import type { MicrosoftRewardsBot } from '../../index'
|
||||
import type { AccountProxy } from '../../interface/Account'
|
||||
|
||||
/**
|
||||
* Create a browser instance for the given account
|
||||
@@ -26,7 +26,7 @@ export async function createBrowserInstance(
|
||||
proxy: AccountProxy,
|
||||
email: string
|
||||
): Promise<BrowserContext> {
|
||||
const browserModule = await import('../browser/Browser')
|
||||
const browserModule = await import('../../browser/Browser')
|
||||
const Browser = browserModule.default
|
||||
const browserInstance = new Browser(bot)
|
||||
return await browserInstance.createBrowser(proxy, email)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import { Util } from './Utils'
|
||||
import type { ConfigHumanization } from '../interface/Config'
|
||||
import type { ConfigHumanization } from '../../interface/Config'
|
||||
import { Util } from '../core/Utils'
|
||||
|
||||
export class Humanizer {
|
||||
private util: Util
|
||||
@@ -46,9 +46,9 @@ export class Humanizer {
|
||||
try {
|
||||
const n = this.util.stringToMs(String(v))
|
||||
return Math.max(0, Math.min(n, 10_000))
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
// Parse failed - use default minimum
|
||||
return defMin
|
||||
return defMin
|
||||
}
|
||||
}
|
||||
min = parse(this.cfg.actionDelay.min)
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../interface/UserAgentUtil'
|
||||
import { log } from './Logger'
|
||||
import { Retry } from './Retry'
|
||||
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../../interface/UserAgentUtil'
|
||||
import { Retry } from '../core/Retry'
|
||||
import { log } from '../notifications/Logger'
|
||||
|
||||
interface UserAgentMetadata {
|
||||
mobile: boolean
|
||||
@@ -95,7 +95,7 @@ export async function getChromeVersion(isMobile: boolean): Promise<string> {
|
||||
|
||||
export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionResult> {
|
||||
const now = Date.now()
|
||||
|
||||
|
||||
// Return cached version if still valid
|
||||
if (edgeVersionCache && edgeVersionCache.expiresAt > now) {
|
||||
return edgeVersionCache.data
|
||||
@@ -123,13 +123,13 @@ export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionRes
|
||||
})
|
||||
.catch(() => {
|
||||
edgeVersionInFlight = null
|
||||
|
||||
|
||||
// Try stale cache first
|
||||
if (edgeVersionCache) {
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using stale cached Edge versions due to fetch failure', 'warn')
|
||||
return edgeVersionCache.data
|
||||
}
|
||||
|
||||
|
||||
// Fall back to static versions
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using static fallback Edge versions (API unavailable)', 'warn')
|
||||
edgeVersionCache = { data: FALLBACK_EDGE_VERSIONS, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS }
|
||||
@@ -192,7 +192,7 @@ async function fetchEdgeVersionsWithRetry(isMobile: boolean): Promise<EdgeVersio
|
||||
|
||||
async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResult> {
|
||||
let lastError: unknown = null
|
||||
|
||||
|
||||
// Try axios first
|
||||
try {
|
||||
const response = await axios<EdgeVersion[]>({
|
||||
@@ -205,11 +205,11 @@ async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResu
|
||||
timeout: 10000,
|
||||
validateStatus: (status) => status === 200
|
||||
})
|
||||
|
||||
|
||||
if (!response.data || !Array.isArray(response.data)) {
|
||||
throw new Error('Invalid response format from Edge API')
|
||||
}
|
||||
|
||||
|
||||
return mapEdgeVersions(response.data)
|
||||
} catch (axiosError) {
|
||||
lastError = axiosError
|
||||
@@ -226,7 +226,7 @@ async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResu
|
||||
} catch (fetchError) {
|
||||
lastError = fetchError
|
||||
}
|
||||
|
||||
|
||||
// Both methods failed
|
||||
const errorMsg = lastError instanceof Error ? lastError.message : String(lastError)
|
||||
throw new Error(`Failed to fetch Edge versions: ${errorMsg}`)
|
||||
@@ -237,7 +237,7 @@ async function tryNativeFetchFallback(): Promise<EdgeVersionResult | null> {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
timeoutHandle = setTimeout(() => controller.abort(), 10000)
|
||||
|
||||
|
||||
const response = await fetch(EDGE_VERSION_URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -245,20 +245,20 @@ async function tryNativeFetchFallback(): Promise<EdgeVersionResult | null> {
|
||||
},
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
|
||||
clearTimeout(timeoutHandle)
|
||||
timeoutHandle = undefined
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json() as EdgeVersion[]
|
||||
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid response format')
|
||||
}
|
||||
|
||||
|
||||
return mapEdgeVersions(data)
|
||||
} catch (error) {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||
@@ -270,24 +270,24 @@ function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
throw new Error('Edge API returned empty or invalid data')
|
||||
}
|
||||
|
||||
|
||||
const stable = data.find(entry => entry?.Product?.toLowerCase() === 'stable')
|
||||
?? data.find(entry => entry?.Product && /stable/i.test(entry.Product))
|
||||
|
||||
|
||||
if (!stable || !stable.Releases || !Array.isArray(stable.Releases)) {
|
||||
throw new Error('Stable Edge channel not found or invalid format')
|
||||
}
|
||||
|
||||
const androidRelease = stable.Releases.find(release =>
|
||||
const androidRelease = stable.Releases.find(release =>
|
||||
release?.Platform === Platform.Android && release?.ProductVersion
|
||||
)
|
||||
|
||||
const windowsRelease = stable.Releases.find(release =>
|
||||
release?.Platform === Platform.Windows &&
|
||||
release?.Architecture === Architecture.X64 &&
|
||||
|
||||
const windowsRelease = stable.Releases.find(release =>
|
||||
release?.Platform === Platform.Windows &&
|
||||
release?.Architecture === Architecture.X64 &&
|
||||
release?.ProductVersion
|
||||
) ?? stable.Releases.find(release =>
|
||||
release?.Platform === Platform.Windows &&
|
||||
) ?? stable.Releases.find(release =>
|
||||
release?.Platform === Platform.Windows &&
|
||||
release?.ProductVersion
|
||||
)
|
||||
|
||||
@@ -295,7 +295,7 @@ function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
|
||||
android: androidRelease?.ProductVersion,
|
||||
windows: windowsRelease?.ProductVersion
|
||||
}
|
||||
|
||||
|
||||
// Validate at least one version was found
|
||||
if (!result.android && !result.windows) {
|
||||
throw new Error('No valid Edge versions found in API response')
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ConfigRetryPolicy } from '../interface/Config'
|
||||
import type { ConfigRetryPolicy } from '../../interface/Config'
|
||||
import { Util } from './Utils'
|
||||
|
||||
type NumericPolicy = {
|
||||
@@ -59,7 +59,7 @@ export class Retry {
|
||||
let attempt = 0
|
||||
let delay = this.policy.baseDelay
|
||||
let lastErr: unknown
|
||||
|
||||
|
||||
while (attempt < this.policy.maxAttempts) {
|
||||
try {
|
||||
return await fn()
|
||||
@@ -2,7 +2,7 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } f
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import { AccountProxy } from '../interface/Account'
|
||||
import { AccountProxy } from '../../interface/Account'
|
||||
|
||||
class AxiosClient {
|
||||
private instance: AxiosInstance
|
||||
@@ -90,13 +90,13 @@ class AxiosClient {
|
||||
// FIXED: Initialize lastError to prevent throwing undefined
|
||||
let lastError: unknown = new Error('Request failed with unknown error')
|
||||
const maxAttempts = 2
|
||||
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await this.instance.request(config)
|
||||
} catch (err: unknown) {
|
||||
lastError = err
|
||||
|
||||
|
||||
// Handle HTTP 407 Proxy Authentication Required
|
||||
if (this.isProxyAuthError(err)) {
|
||||
// Retry without proxy on auth failure
|
||||
@@ -116,15 +116,15 @@ class AxiosClient {
|
||||
const bypassInstance = axios.create()
|
||||
return bypassInstance.request(config)
|
||||
}
|
||||
|
||||
|
||||
// Non-retryable error
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if error is HTTP 407 Proxy Authentication Required
|
||||
*/
|
||||
@@ -132,27 +132,27 @@ class AxiosClient {
|
||||
const axiosErr = err as AxiosError | undefined
|
||||
return axiosErr?.response?.status === 407
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if error is retryable (network/proxy issues)
|
||||
*/
|
||||
private isRetryableError(err: unknown): boolean {
|
||||
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
|
||||
if (!e) return false
|
||||
|
||||
|
||||
const code = e.code || e.cause?.code
|
||||
const isNetworkError = code === 'ECONNREFUSED' ||
|
||||
code === 'ETIMEDOUT' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'ENOTFOUND' ||
|
||||
code === 'EPIPE'
|
||||
|
||||
const isNetworkError = code === 'ECONNREFUSED' ||
|
||||
code === 'ETIMEDOUT' ||
|
||||
code === 'ECONNRESET' ||
|
||||
code === 'ENOTFOUND' ||
|
||||
code === 'EPIPE'
|
||||
|
||||
const msg = String(e.message || '')
|
||||
const isProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
|
||||
|
||||
|
||||
return isNetworkError || isProxyIssue
|
||||
}
|
||||
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { Util } from './Utils'
|
||||
import { Util } from '../core/Utils'
|
||||
|
||||
export interface QueryDiversityConfig {
|
||||
sources: Array<'google-trends' | 'reddit' | 'news' | 'wikipedia' | 'local-fallback'>
|
||||
@@ -22,10 +22,10 @@ export class QueryDiversityEngine {
|
||||
constructor(config?: Partial<QueryDiversityConfig>, logger?: (source: string, message: string, level?: 'info' | 'warn' | 'error') => void) {
|
||||
const maxQueriesPerSource = Math.max(1, Math.min(config?.maxQueriesPerSource || 10, 50))
|
||||
const cacheMinutes = Math.max(1, Math.min(config?.cacheMinutes || 30, 1440))
|
||||
|
||||
|
||||
this.config = {
|
||||
sources: config?.sources && config.sources.length > 0
|
||||
? config.sources
|
||||
sources: config?.sources && config.sources.length > 0
|
||||
? config.sources
|
||||
: ['google-trends', 'reddit', 'local-fallback'],
|
||||
deduplicate: config?.deduplicate !== false,
|
||||
mixStrategies: config?.mixStrategies !== false,
|
||||
@@ -44,7 +44,7 @@ export class QueryDiversityEngine {
|
||||
/**
|
||||
* Generic HTTP fetch with error handling and timeout
|
||||
*/
|
||||
private async fetchHttp(url: string, config?: {
|
||||
private async fetchHttp(url: string, config?: {
|
||||
method?: 'GET' | 'POST'
|
||||
headers?: Record<string, string>
|
||||
data?: string
|
||||
@@ -104,7 +104,7 @@ export class QueryDiversityEngine {
|
||||
*/
|
||||
private async getFromSource(source: string): Promise<string[]> {
|
||||
this.cleanExpiredCache()
|
||||
|
||||
|
||||
const cached = this.cache.get(source)
|
||||
if (cached && Date.now() < cached.expires) {
|
||||
return cached.queries
|
||||
@@ -174,7 +174,7 @@ export class QueryDiversityEngine {
|
||||
try {
|
||||
const subreddits = ['news', 'worldnews', 'todayilearned', 'askreddit', 'technology']
|
||||
const randomSub = subreddits[Math.floor(Math.random() * subreddits.length)]
|
||||
|
||||
|
||||
const data = await this.fetchHttp(`https://www.reddit.com/r/${randomSub}/hot.json?limit=15`)
|
||||
const parsed = JSON.parse(data)
|
||||
const posts = parsed.data?.children || []
|
||||
@@ -296,28 +296,28 @@ export class QueryDiversityEngine {
|
||||
const result: string[] = []
|
||||
const queriesPerSource = Math.ceil(this.config.maxQueriesPerSource)
|
||||
const sourceCount = this.config.sources.length
|
||||
|
||||
|
||||
if (sourceCount === 0 || queries.length === 0) {
|
||||
return queries.slice(0, targetCount)
|
||||
}
|
||||
|
||||
|
||||
const chunkSize = queriesPerSource
|
||||
let sourceIndex = 0
|
||||
|
||||
|
||||
for (let i = 0; i < queries.length && result.length < targetCount; i++) {
|
||||
const currentChunkStart = sourceIndex * chunkSize
|
||||
const currentChunkEnd = currentChunkStart + chunkSize
|
||||
const query = queries[i]
|
||||
|
||||
|
||||
if (query && i >= currentChunkStart && i < currentChunkEnd) {
|
||||
result.push(query)
|
||||
}
|
||||
|
||||
|
||||
if (i === currentChunkEnd - 1) {
|
||||
sourceIndex = (sourceIndex + 1) % sourceCount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result.slice(0, targetCount)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import { Config } from '../interface/Config'
|
||||
import { Ntfy } from './Ntfy'
|
||||
import { DISCORD } from '../../constants'
|
||||
import { Config } from '../../interface/Config'
|
||||
import { log } from './Logger'
|
||||
import { DISCORD } from '../constants'
|
||||
import { Ntfy } from './Ntfy'
|
||||
|
||||
interface DiscordField {
|
||||
name: string
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import { DISCORD } from '../constants'
|
||||
import { Config } from '../interface/Config'
|
||||
import { DISCORD } from '../../constants'
|
||||
import { Config } from '../../interface/Config'
|
||||
|
||||
interface ErrorReportPayload {
|
||||
error: string
|
||||
@@ -35,7 +35,7 @@ export function deobfuscateWebhookUrl(encoded: string): string {
|
||||
*/
|
||||
function shouldReportError(errorMessage: string): boolean {
|
||||
const lowerMessage = errorMessage.toLowerCase()
|
||||
|
||||
|
||||
// List of patterns that indicate user configuration errors (not reportable bugs)
|
||||
const userConfigPatterns = [
|
||||
/accounts\.jsonc.*not found/i,
|
||||
@@ -59,14 +59,14 @@ function shouldReportError(errorMessage: string): boolean {
|
||||
/session closed.*rebrowser/i,
|
||||
/addScriptToEvaluateOnNewDocument.*session closed/i
|
||||
]
|
||||
|
||||
|
||||
// Don't report user configuration errors
|
||||
for (const pattern of userConfigPatterns) {
|
||||
if (pattern.test(lowerMessage)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// List of patterns that indicate expected/handled errors (not bugs)
|
||||
const expectedErrorPatterns = [
|
||||
/no.*points.*to.*earn/i,
|
||||
@@ -76,14 +76,14 @@ function shouldReportError(errorMessage: string): boolean {
|
||||
/quest.*not.*found/i,
|
||||
/promotion.*expired/i
|
||||
]
|
||||
|
||||
|
||||
// Don't report expected/handled errors
|
||||
for (const pattern of expectedErrorPatterns) {
|
||||
if (pattern.test(lowerMessage)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Report everything else (genuine bugs)
|
||||
return true
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export async function sendErrorReport(
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
|
||||
// Filter out false positives and user configuration errors
|
||||
if (!shouldReportError(errorMessage)) {
|
||||
return
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import { DISCORD, LOGGER_CLEANUP } from '../constants'
|
||||
import { DISCORD, LOGGER_CLEANUP } from '../../constants'
|
||||
import { loadConfig } from '../state/Load'
|
||||
import { sendErrorReport } from './ErrorReportingWebhook'
|
||||
import { loadConfig } from './Load'
|
||||
import { Ntfy } from './Ntfy'
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loadConfig } from './Load'
|
||||
import axios from 'axios'
|
||||
import { loadConfig } from '../state/Load'
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
error: { priority: 'max', tags: 'rotating_light' }, // Customize the ERROR icon here, see: https://docs.ntfy.sh/emojis/
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import type { Config } from '../interface/Config'
|
||||
import type { Config } from '../../interface/Config'
|
||||
|
||||
type AccountCompletionMeta = {
|
||||
runId?: string
|
||||
@@ -2,9 +2,9 @@ import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { BrowserContext, Cookie } from 'rebrowser-playwright'
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../interface/Config'
|
||||
import { Util } from './Utils'
|
||||
import { Account } from '../../interface/Account'
|
||||
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../../interface/Config'
|
||||
import { Util } from '../core/Utils'
|
||||
|
||||
const utils = new Util()
|
||||
|
||||
@@ -76,16 +76,16 @@ function normalizeConfig(raw: unknown): Config {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
throw new Error('Config must be a valid object')
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const n = raw as Record<string, any>
|
||||
|
||||
// Browser settings
|
||||
const browserConfig = n.browser ?? {}
|
||||
const headless = process.env.FORCE_HEADLESS === '1'
|
||||
? true
|
||||
: (typeof browserConfig.headless === 'boolean'
|
||||
? browserConfig.headless
|
||||
const headless = process.env.FORCE_HEADLESS === '1'
|
||||
? true
|
||||
: (typeof browserConfig.headless === 'boolean'
|
||||
? browserConfig.headless
|
||||
: (typeof n.headless === 'boolean' ? n.headless : false)) // Legacy fallback
|
||||
|
||||
const globalTimeout = browserConfig.globalTimeout ?? n.globalTimeout ?? '30s'
|
||||
@@ -339,12 +339,12 @@ export function loadAccounts(): Account[] {
|
||||
]
|
||||
let chosen: string | null = null
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
chosen = p
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
// Filesystem check failed for this path, try next
|
||||
continue
|
||||
}
|
||||
@@ -365,12 +365,12 @@ export function loadAccounts(): Account[] {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
throw new Error('each account entry must be an object')
|
||||
}
|
||||
|
||||
|
||||
// Use Record<string, any> to access dynamic properties from untrusted JSON
|
||||
// Runtime validation below ensures type safety
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const a = entry as Record<string, any>
|
||||
|
||||
|
||||
// Validate required fields with proper type checking
|
||||
if (typeof a.email !== 'string' || typeof a.password !== 'string') {
|
||||
throw new Error('each account must have email and password strings')
|
||||
@@ -439,15 +439,15 @@ export function loadConfig(): Config {
|
||||
candidates.push(path.join(base, name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let cfgPath: string | null = null
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
cfgPath = p
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
// Filesystem check failed for this path, try next
|
||||
continue
|
||||
}
|
||||
@@ -517,7 +517,7 @@ export async function saveSessionData(sessionPath: string, browser: BrowserConte
|
||||
|
||||
// Save cookies to a file
|
||||
await fs.promises.writeFile(
|
||||
path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`),
|
||||
path.join(sessionDir, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`),
|
||||
JSON.stringify(cookies, null, 2)
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import chalk from 'chalk'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { Account } from '../interface/Account'
|
||||
import { Config } from '../interface/Config'
|
||||
import { log } from './Logger'
|
||||
import { Account } from '../../interface/Account'
|
||||
import { Config } from '../../interface/Config'
|
||||
import { log } from '../notifications/Logger'
|
||||
|
||||
interface ValidationError {
|
||||
severity: 'error' | 'warning'
|
||||
@@ -181,12 +181,12 @@ export class StartupValidator {
|
||||
private validateConfig(config: Config): void {
|
||||
const maybeSchedule = (config as unknown as { schedule?: unknown }).schedule
|
||||
if (maybeSchedule !== undefined) {
|
||||
this.addWarning(
|
||||
'config',
|
||||
'Legacy schedule settings detected in config.jsonc.',
|
||||
'Remove schedule.* entries and use your operating system scheduler.',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
this.addWarning(
|
||||
'config',
|
||||
'Legacy schedule settings detected in config.jsonc.',
|
||||
'Remove schedule.* entries and use your operating system scheduler.',
|
||||
'docs/schedule.md'
|
||||
)
|
||||
}
|
||||
|
||||
// Headless mode in Docker
|
||||
@@ -218,10 +218,10 @@ export class StartupValidator {
|
||||
}
|
||||
|
||||
// Global timeout validation
|
||||
const timeout = typeof config.globalTimeout === 'string'
|
||||
? config.globalTimeout
|
||||
const timeout = typeof config.globalTimeout === 'string'
|
||||
? config.globalTimeout
|
||||
: `${config.globalTimeout}ms`
|
||||
|
||||
|
||||
if (timeout === '0' || timeout === '0ms' || timeout === '0s') {
|
||||
this.addError(
|
||||
'config',
|
||||
@@ -271,7 +271,7 @@ export class StartupValidator {
|
||||
// Node.js version check
|
||||
const nodeVersion = process.version
|
||||
const major = parseInt(nodeVersion.split('.')[0]?.replace('v', '') || '0', 10)
|
||||
|
||||
|
||||
if (major < 18) {
|
||||
this.addError(
|
||||
'environment',
|
||||
@@ -329,10 +329,10 @@ export class StartupValidator {
|
||||
|
||||
// Check job-state directory if enabled
|
||||
if (config.jobState?.enabled !== false) {
|
||||
const jobStateDir = config.jobState?.dir
|
||||
? config.jobState.dir
|
||||
const jobStateDir = config.jobState?.dir
|
||||
? config.jobState.dir
|
||||
: path.join(sessionPath, 'job-state')
|
||||
|
||||
|
||||
if (!fs.existsSync(jobStateDir)) {
|
||||
try {
|
||||
fs.mkdirSync(jobStateDir, { recursive: true })
|
||||
@@ -428,12 +428,12 @@ export class StartupValidator {
|
||||
|
||||
private validateWorkerSettings(config: Config): void {
|
||||
const workers = config.workers
|
||||
|
||||
|
||||
// Check if at least one worker is enabled
|
||||
const anyEnabled = workers.doDailySet || workers.doMorePromotions || workers.doPunchCards ||
|
||||
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
|
||||
workers.doReadToEarn
|
||||
|
||||
workers.doDesktopSearch || workers.doMobileSearch || workers.doDailyCheckIn ||
|
||||
workers.doReadToEarn
|
||||
|
||||
if (!anyEnabled) {
|
||||
this.addWarning(
|
||||
'workers',
|
||||
@@ -465,7 +465,7 @@ export class StartupValidator {
|
||||
private validateExecutionSettings(config: Config): void {
|
||||
// Validate passesPerRun
|
||||
const passes = config.passesPerRun ?? 1
|
||||
|
||||
|
||||
if (passes < 1) {
|
||||
this.addError(
|
||||
'execution',
|
||||
@@ -595,8 +595,8 @@ export class StartupValidator {
|
||||
|
||||
// Action delays
|
||||
if (human.actionDelay) {
|
||||
const minMs = typeof human.actionDelay.min === 'string'
|
||||
? parseInt(human.actionDelay.min, 10)
|
||||
const minMs = typeof human.actionDelay.min === 'string'
|
||||
? parseInt(human.actionDelay.min, 10)
|
||||
: human.actionDelay.min
|
||||
const maxMs = typeof human.actionDelay.max === 'string'
|
||||
? parseInt(human.actionDelay.max, 10)
|
||||
@@ -717,7 +717,7 @@ export class StartupValidator {
|
||||
const errorLabel = this.errors.length === 1 ? 'error' : 'errors'
|
||||
const warningLabel = this.warnings.length === 1 ? 'warning' : 'warnings'
|
||||
log('main', 'VALIDATION', `[${this.errors.length > 0 ? 'ERROR' : 'OK'}] Found: ${this.errors.length} ${errorLabel} | ${this.warnings.length} ${warningLabel}`)
|
||||
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
log('main', 'VALIDATION', 'Bot will continue, but issues may cause failures', 'warn')
|
||||
log('main', 'VALIDATION', 'Full documentation: docs/index.md')
|
||||
Reference in New Issue
Block a user