mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 00:56:16 +00:00
fix: Update error message for HTTP 400 checks to English refactor: Improve logging for network idle wait timeout in Workers fix: Initialize lastError in Axios client to prevent undefined errors chore: Update tsconfig to include path mappings for better module resolution
278 lines
12 KiB
TypeScript
278 lines
12 KiB
TypeScript
import { Page } from 'rebrowser-playwright'
|
|
|
|
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'
|
|
|
|
// 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)`
|
|
} as const
|
|
|
|
export class Workers {
|
|
public bot: MicrosoftRewardsBot
|
|
private jobState: JobState
|
|
|
|
constructor(bot: MicrosoftRewardsBot) {
|
|
this.bot = bot
|
|
this.jobState = new JobState(this.bot.config)
|
|
}
|
|
|
|
// Daily Set
|
|
async doDailySet(page: Page, data: DashboardData) {
|
|
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
|
|
|
|
const today = this.bot.utils.getFormattedDate()
|
|
const activitiesUncompleted = (todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [])
|
|
.filter(x => {
|
|
if (this.bot.config.jobState?.enabled === false) return true
|
|
const email = this.bot.currentAccountEmail || 'unknown'
|
|
return !this.jobState.isDone(email, today, x.offerId)
|
|
})
|
|
|
|
if (!activitiesUncompleted.length) {
|
|
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
|
|
return
|
|
}
|
|
|
|
// Solve Activities
|
|
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'Started solving "Daily Set" items')
|
|
|
|
await this.solveActivities(page, activitiesUncompleted)
|
|
|
|
// Mark as done to prevent duplicate work if checkpoints enabled
|
|
if (this.bot.config.jobState?.enabled !== false) {
|
|
const email = this.bot.currentAccountEmail || 'unknown'
|
|
for (const a of activitiesUncompleted) {
|
|
this.jobState.markDone(email, today, a.offerId)
|
|
}
|
|
}
|
|
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
// Always return to the homepage if not already
|
|
await this.bot.browser.func.goHome(page)
|
|
|
|
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have been completed')
|
|
|
|
// Optional: immediately run desktop search bundle
|
|
if (!this.bot.isMobile && this.bot.config.workers.bundleDailySetWithSearch && this.bot.config.workers.doDesktopSearch) {
|
|
try {
|
|
await this.bot.utils.waitRandom(1200, 2600)
|
|
await this.bot.activities.doSearch(page, data)
|
|
} catch (e) {
|
|
this.bot.log(this.bot.isMobile, 'DAILY-SET', `Post-DailySet search failed: ${e instanceof Error ? e.message : e}`, 'warn')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Punch Card
|
|
async doPunchCard(page: Page, data: DashboardData) {
|
|
|
|
const punchCardsUncompleted = data.punchCards?.filter(x => x.parentPromotion && !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
|
|
|
|
if (!punchCardsUncompleted.length) {
|
|
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Cards" have already been completed')
|
|
return
|
|
}
|
|
|
|
for (const punchCard of punchCardsUncompleted) {
|
|
|
|
// Ensure parentPromotion exists before proceeding
|
|
if (!punchCard.parentPromotion?.title) {
|
|
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Skipped punchcard "${punchCard.name}" | Reason: Parent promotion is missing!`, 'warn')
|
|
continue
|
|
}
|
|
|
|
// Get latest page for each card
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
|
|
|
|
// Solve Activities
|
|
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
|
|
|
|
// Got to punch card index page in a new tab
|
|
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
|
|
|
|
// Wait for new page to load, max 10 seconds, however try regardless in case of error
|
|
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(logError('PUNCH-CARD', 'Network idle wait timeout (non-critical)', this.bot.isMobile))
|
|
|
|
await this.solveActivities(page, activitiesUncompleted, punchCard)
|
|
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
const pages = page.context().pages()
|
|
|
|
if (pages.length > 3) {
|
|
await page.close()
|
|
} else {
|
|
await this.bot.browser.func.goHome(page)
|
|
}
|
|
|
|
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
|
|
}
|
|
|
|
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Card" items have been completed')
|
|
}
|
|
|
|
// More Promotions
|
|
async doMorePromotions(page: Page, data: DashboardData) {
|
|
const morePromotions = data.morePromotions
|
|
|
|
// Check if there is a promotional item
|
|
if (data.promotionalItem) { // Convert and add the promotional item to the array
|
|
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
|
|
}
|
|
|
|
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && x.exclusiveLockedFeatureStatus !== 'locked') ?? []
|
|
|
|
if (!activitiesUncompleted.length) {
|
|
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
|
|
return
|
|
}
|
|
|
|
// Solve Activities
|
|
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'Started solving "More Promotions" items')
|
|
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
await this.solveActivities(page, activitiesUncompleted)
|
|
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
// Always return to the homepage if not already
|
|
await this.bot.browser.func.goHome(page)
|
|
|
|
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
|
|
}
|
|
|
|
// Solve all the different types of activities
|
|
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
|
|
const activityInitial = activityPage.url()
|
|
const retry = new Retry(this.bot.config.retryPolicy)
|
|
const throttle = new AdaptiveThrottler()
|
|
|
|
for (const activity of activities) {
|
|
try {
|
|
activityPage = await this.manageTabLifecycle(activityPage, activityInitial)
|
|
await this.applyThrottle(throttle, 800, 1400)
|
|
|
|
const selector = await this.buildActivitySelector(activityPage, activity, punchCard)
|
|
await this.prepareActivityPage(activityPage, selector, throttle)
|
|
|
|
const typeLabel = this.bot.activities.getTypeLabel(activity)
|
|
if (typeLabel !== 'Unsupported') {
|
|
await this.executeActivity(activityPage, activity, selector, throttle, retry)
|
|
} else {
|
|
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
|
|
}
|
|
|
|
await this.applyThrottle(throttle, 1200, 2600)
|
|
} catch (error) {
|
|
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
|
|
throttle.record(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async manageTabLifecycle(page: Page, initialUrl: string): Promise<Page> {
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
const pages = page.context().pages()
|
|
if (pages.length > 3) {
|
|
await page.close()
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
}
|
|
|
|
if (page.url() !== initialUrl) {
|
|
await page.goto(initialUrl)
|
|
}
|
|
|
|
return page
|
|
}
|
|
|
|
private async buildActivitySelector(page: Page, activity: PromotionalItem | MorePromotion, punchCard?: PunchCard): Promise<string> {
|
|
if (punchCard) {
|
|
return await this.bot.browser.func.getPunchCardActivity(page, activity)
|
|
}
|
|
|
|
const name = activity.name.toLowerCase()
|
|
if (name.includes('membercenter') || name.includes('exploreonbing')) {
|
|
return ACTIVITY_SELECTORS.byName(activity.name)
|
|
}
|
|
|
|
// Validate offerId exists before using it in selector
|
|
if (!activity.offerId) {
|
|
this.bot.log(this.bot.isMobile, 'WORKERS', `Activity "${activity.name || activity.title}" has no offerId, falling back to name-based selector`, 'warn')
|
|
return ACTIVITY_SELECTORS.byName(activity.name)
|
|
}
|
|
|
|
return ACTIVITY_SELECTORS.byOfferId(activity.offerId)
|
|
}
|
|
|
|
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile))
|
|
await this.bot.browser.utils.humanizePage(page)
|
|
await this.applyThrottle(throttle, 1200, 2600)
|
|
}
|
|
|
|
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: 5000 })
|
|
} catch (error) {
|
|
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity selector not found (might be completed or unavailable): ${selector}`, 'warn')
|
|
return // Skip this activity gracefully instead of waiting 30s
|
|
}
|
|
|
|
// Click with timeout to prevent indefinite hangs
|
|
await page.click(selector, { timeout: 10000 })
|
|
page = await this.bot.browser.utils.getLatestTab(page)
|
|
|
|
// FIXED: Use AbortController for proper cancellation instead of race condition
|
|
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
|
|
const controller = new AbortController()
|
|
const timeoutHandle = setTimeout(() => {
|
|
controller.abort()
|
|
}, timeoutMs)
|
|
|
|
const runWithTimeout = async (p: Promise<void>) => {
|
|
try {
|
|
await p
|
|
clearTimeout(timeoutHandle)
|
|
} catch (error) {
|
|
clearTimeout(timeoutHandle)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
await retry.run(async () => {
|
|
try {
|
|
await runWithTimeout(this.bot.activities.run(page, activity))
|
|
throttle.record(true)
|
|
} catch (e) {
|
|
throttle.record(false)
|
|
throw e
|
|
}
|
|
}, () => true)
|
|
|
|
await this.bot.browser.utils.humanizePage(page)
|
|
}
|
|
|
|
private async applyThrottle(throttle: AdaptiveThrottler, min: number, max: number): Promise<void> {
|
|
const multiplier = throttle.getDelayMultiplier()
|
|
await this.bot.utils.waitRandom(
|
|
Math.floor(min * multiplier),
|
|
Math.floor(max * multiplier)
|
|
)
|
|
}
|
|
|
|
} |