mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
feature: Add the BuyModeManual module to manage manual purchase sessions
This commit is contained in:
197
src/flows/BuyModeManual.ts
Normal file
197
src/flows/BuyModeManual.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Buy Mode Manual Handler Module
|
||||||
|
* Extracted from index.ts runBuyMode() method
|
||||||
|
*
|
||||||
|
* Provides manual spending mode where user retains control:
|
||||||
|
* - Opens two browser tabs (monitor + browsing)
|
||||||
|
* - Passively monitors point changes every ~10s
|
||||||
|
* - Detects spending and sends notifications
|
||||||
|
* - Session has configurable duration (default: 45 minutes)
|
||||||
|
* - User manually selects and purchases items
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserContext } from 'playwright'
|
||||||
|
import type { MicrosoftRewardsBot } from '../index'
|
||||||
|
import type { Account } from '../interface/Account'
|
||||||
|
import { BuyModeMonitor, BuyModeSelector } from '../util/BuyMode'
|
||||||
|
import { saveSessionData } from '../util/Load'
|
||||||
|
import { log } from '../util/Logger'
|
||||||
|
|
||||||
|
interface AccountSummary {
|
||||||
|
email: string
|
||||||
|
durationMs: number
|
||||||
|
desktopCollected: number
|
||||||
|
mobileCollected: number
|
||||||
|
totalCollected: number
|
||||||
|
initialTotal: number
|
||||||
|
endTotal: number
|
||||||
|
errors: string[]
|
||||||
|
banned: { status: boolean; reason: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BuyModeManual {
|
||||||
|
private bot: MicrosoftRewardsBot
|
||||||
|
|
||||||
|
constructor(bot: MicrosoftRewardsBot) {
|
||||||
|
this.bot = bot
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute manual buy mode session
|
||||||
|
* Opens browser, logs in, and passively monitors points while user browses/purchases
|
||||||
|
* @param buyModeArgument Optional account email to use (otherwise prompts user)
|
||||||
|
* @returns Promise that resolves when session ends
|
||||||
|
*/
|
||||||
|
async execute(buyModeArgument?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const buyModeConfig = this.bot.config.buyMode as { maxMinutes?: number } | undefined
|
||||||
|
const maxMinutes = buyModeConfig?.maxMinutes ?? 45
|
||||||
|
|
||||||
|
// Access private accounts array via type assertion
|
||||||
|
const accounts = (this.bot as unknown as { accounts: Account[] }).accounts
|
||||||
|
const selector = new BuyModeSelector(accounts)
|
||||||
|
const selection = await selector.selectAccount(buyModeArgument, maxMinutes)
|
||||||
|
|
||||||
|
if (!selection) {
|
||||||
|
log('main', 'BUY-MODE', 'Buy mode cancelled: no account selected', 'warn')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { account, maxMinutes: sessionMaxMinutes } = selection
|
||||||
|
|
||||||
|
log('main', 'BUY-MODE', `Buy mode ENABLED for ${account.email}. Opening 2 tabs: (1) monitor tab (auto-refresh), (2) your browsing tab`, 'log', 'green')
|
||||||
|
log('main', 'BUY-MODE', `Session duration: ${sessionMaxMinutes} minutes. Monitor tab refreshes every ~10s. Use the other tab for your actions.`, 'log', 'yellow')
|
||||||
|
|
||||||
|
this.bot.isMobile = false
|
||||||
|
|
||||||
|
// Access private browserFactory via type assertion
|
||||||
|
const browserFactory = (this.bot as unknown as { browserFactory: { createBrowser: (proxy: Account['proxy'], email: string) => Promise<BrowserContext> } }).browserFactory
|
||||||
|
const browser = await browserFactory.createBrowser(account.proxy, account.email)
|
||||||
|
|
||||||
|
// Open the monitor tab FIRST so auto-refresh happens out of the way
|
||||||
|
let monitor = await browser.newPage()
|
||||||
|
|
||||||
|
// Access private login via type assertion (use 'any' for internal page type - unavoidable)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const loginModule = (this.bot as any).login
|
||||||
|
await loginModule.login(monitor, account.email, account.password, account.totp)
|
||||||
|
await this.bot.browser.func.goHome(monitor)
|
||||||
|
this.bot.log(false, 'BUY-MODE', 'Opened MONITOR tab (auto-refreshes to track points).', 'log', 'yellow')
|
||||||
|
|
||||||
|
// Then open the user free-browsing tab SECOND so users don't see the refreshes
|
||||||
|
const page = await browser.newPage()
|
||||||
|
await this.bot.browser.func.goHome(page)
|
||||||
|
this.bot.log(false, 'BUY-MODE', 'Opened USER tab (use this one to redeem/purchase freely).', 'log', 'green')
|
||||||
|
|
||||||
|
// Helper to recreate monitor tab if the user closes it
|
||||||
|
const recreateMonitor = async () => {
|
||||||
|
try { if (!monitor.isClosed()) await monitor.close() } catch { /* ignore */ }
|
||||||
|
monitor = await browser.newPage()
|
||||||
|
await this.bot.browser.func.goHome(monitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to send an immediate spend notice via webhooks/NTFY
|
||||||
|
const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => {
|
||||||
|
try {
|
||||||
|
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
|
||||||
|
await ConclusionWebhook(
|
||||||
|
this.bot.config,
|
||||||
|
'💳 Spend Detected',
|
||||||
|
`**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`,
|
||||||
|
undefined,
|
||||||
|
0xFFAA00
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial points
|
||||||
|
let initial = 0
|
||||||
|
try {
|
||||||
|
const data = await this.bot.browser.func.getDashboardData(monitor)
|
||||||
|
initial = data.userStatus.availablePoints || 0
|
||||||
|
} catch {/* ignore */}
|
||||||
|
|
||||||
|
const pointMonitor = new BuyModeMonitor(initial)
|
||||||
|
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Logged in as ${account.email}. Starting passive point monitoring (session: ${sessionMaxMinutes} min)`)
|
||||||
|
|
||||||
|
// Passive watcher: poll points periodically without clicking.
|
||||||
|
const start = Date.now()
|
||||||
|
const endAt = start + sessionMaxMinutes * 60 * 1000
|
||||||
|
|
||||||
|
while (Date.now() < endAt) {
|
||||||
|
await this.bot.utils.wait(10000)
|
||||||
|
|
||||||
|
// If monitor tab was closed by user, recreate it quietly
|
||||||
|
try {
|
||||||
|
if (monitor.isClosed()) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', 'Monitor tab was closed; reopening in background...', 'warn')
|
||||||
|
await recreateMonitor()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Failed to check/recreate monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.bot.browser.func.getDashboardData(monitor)
|
||||||
|
const nowPts = data.userStatus.availablePoints || 0
|
||||||
|
|
||||||
|
const spendInfo = pointMonitor.checkSpending(nowPts)
|
||||||
|
if (spendInfo) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`)
|
||||||
|
await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// If we lost the page context, recreate the monitor tab and continue
|
||||||
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
if (/Target closed|page has been closed|browser has been closed/i.test(msg)) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', 'Monitor page closed or lost; recreating...', 'warn')
|
||||||
|
try {
|
||||||
|
await recreateMonitor()
|
||||||
|
} catch (e) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Failed to recreate monitor: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Dashboard check error: ${msg}`, 'warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save cookies and close monitor; keep main page open for user until they close it themselves
|
||||||
|
try {
|
||||||
|
await saveSessionData(this.bot.config.sessionPath, browser, account.email, this.bot.isMobile)
|
||||||
|
} catch (e) {
|
||||||
|
log(false, 'BUY-MODE', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!monitor.isClosed()) await monitor.close()
|
||||||
|
} catch (e) {
|
||||||
|
log(false, 'BUY-MODE', `Failed to close monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a final minimal conclusion webhook for this manual session
|
||||||
|
const monitorSummary = pointMonitor.getSummary()
|
||||||
|
const summary: AccountSummary = {
|
||||||
|
email: account.email,
|
||||||
|
durationMs: monitorSummary.duration,
|
||||||
|
desktopCollected: 0,
|
||||||
|
mobileCollected: 0,
|
||||||
|
totalCollected: -monitorSummary.spent, // negative indicates spend
|
||||||
|
initialTotal: monitorSummary.initial,
|
||||||
|
endTotal: monitorSummary.current,
|
||||||
|
errors: [],
|
||||||
|
banned: { status: false, reason: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access private sendConclusion via type assertion
|
||||||
|
const sendConclusion = (this.bot as unknown as { sendConclusion: (summaries: AccountSummary[]) => Promise<void> }).sendConclusion
|
||||||
|
await sendConclusion.call(this.bot, [summary])
|
||||||
|
|
||||||
|
this.bot.log(false, 'BUY-MODE', 'Buy mode session finished (monitoring period ended). You can close the browser when done.')
|
||||||
|
} catch (e) {
|
||||||
|
this.bot.log(false, 'BUY-MODE', `Error in buy mode: ${e instanceof Error ? e.message : String(e)}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user