Files
Microsoft-Rewards-Bot/src/flows/SummaryReporter.ts

392 lines
15 KiB
TypeScript

/**
* Summary Reporter Module
* Extracted from index.ts to improve maintainability and testability
*
* Handles reporting and notifications:
* - Points collection summaries
* - Webhook notifications
* - Ntfy push notifications
* - Job state updates
* - Account history tracking (all-time stats)
*/
import { Account } from '../interface/Account'
import type { Config } from '../interface/Config'
import { ConclusionWebhook } from '../util/notifications/ConclusionWebhook'
import { log } from '../util/notifications/Logger'
import { Ntfy } from '../util/notifications/Ntfy'
import { AccountHistory, AccountHistoryEntry } from '../util/state/AccountHistory'
import { getActivityStatsTracker, resetActivityStatsTracker } from '../util/state/ActivityStatsTracker'
import { JobState } from '../util/state/JobState'
export interface AccountResult {
email: string
pointsEarned: number
runDuration: number
initialPoints: number // Points before execution
finalPoints: number // Points after execution
desktopPoints: number // Points earned on desktop
mobilePoints: number // Points earned on mobile
errors?: string[]
banned?: boolean
}
export interface SummaryData {
accounts: AccountResult[]
startTime: Date
endTime: Date
totalPoints: number
successCount: number
failureCount: number
}
export class SummaryReporter {
private config: Config
private jobState?: JobState
private accountHistory: AccountHistory
constructor(config: Config, accounts: Account[]) {
this.config = config
if (config.jobState?.enabled !== false) {
this.jobState = new JobState(config)
}
this.accountHistory = new AccountHistory(accounts)
}
/**
* Send comprehensive summary via webhook with complete statistics
*/
async sendWebhookSummary(summary: SummaryData): Promise<void> {
if (!this.config.webhook?.enabled && !this.config.conclusionWebhook?.enabled) {
return
}
try {
const duration = Math.round((summary.endTime.getTime() - summary.startTime.getTime()) / 1000)
const hours = Math.floor(duration / 3600)
const minutes = Math.floor((duration % 3600) / 60)
const seconds = duration % 60
const durationText = hours > 0
? `${hours}h ${minutes}m ${seconds}s`
: minutes > 0
? `${minutes}m ${seconds}s`
: `${seconds}s`
// Calculate global statistics
const totalDesktop = summary.accounts.reduce((sum, acc) => sum + acc.desktopPoints, 0)
const totalMobile = summary.accounts.reduce((sum, acc) => sum + acc.mobilePoints, 0)
const totalInitial = summary.accounts.reduce((sum, acc) => sum + acc.initialPoints, 0)
const totalFinal = summary.accounts.reduce((sum, acc) => sum + acc.finalPoints, 0)
const bannedCount = summary.accounts.filter(acc => acc.banned).length
// Build clean, Discord-optimized description
let description = `**⏱️ Duration:** ${durationText}\n`
description += `**💰 Total Earned:** ${summary.totalPoints} points\n`
description += `**🖥️ Desktop:** ${totalDesktop} pts | **📱 Mobile:** ${totalMobile} pts\n`
description += `**✅ Success:** ${summary.successCount}/${summary.accounts.length}`
if (summary.failureCount > 0) {
description += ` | **❌ Failed:** ${summary.failureCount}`
}
if (bannedCount > 0) {
description += ` | **🚫 Banned:** ${bannedCount}`
}
description += '\n\n**📊 Account Details**\n'
const accountsWithErrors: AccountResult[] = []
for (const account of summary.accounts) {
const status = account.banned ? '🚫' : (account.errors?.length ? '❌' : '✅')
const emailShort = account.email.length > 35 ? account.email.substring(0, 32) + '...' : account.email
const durationSec = Math.round(account.runDuration / 1000)
description += `\n${status} **${emailShort}**\n`
description += `• Points: **+${account.pointsEarned}** (🖥️ ${account.desktopPoints} | 📱 ${account.mobilePoints})\n`
description += `• Balance: ${account.initialPoints} → **${account.finalPoints}** pts\n`
description += `• Duration: ${durationSec}s\n`
// Collect accounts with errors for separate webhook
if (this.hasAccountFailure(account)) {
accountsWithErrors.push(account)
}
}
// Footer summary
description += '\n**🌐 Total Balance**\n'
description += `${totalInitial} → **${totalFinal}** pts (+${summary.totalPoints})`
const color = bannedCount > 0 ? 0xFF0000 : summary.failureCount > 0 ? 0xFFAA00 : 0x00FF00
// Send main summary webhook
await ConclusionWebhook(
this.config,
'🎉 Daily Rewards Collection Complete',
description,
undefined,
color
)
// Send separate error report if there are accounts with issues
if (accountsWithErrors.length > 0) {
await this.sendErrorReport(accountsWithErrors)
}
} catch (error) {
log('main', 'SUMMARY', `Failed to send webhook: ${error instanceof Error ? error.message : String(error)}`, 'error')
}
}
/**
* Send separate webhook for accounts with errors or bans
*/
private async sendErrorReport(accounts: AccountResult[]): Promise<void> {
try {
let errorDescription = `**${accounts.length} account(s) encountered issues:**\n\n`
for (const account of accounts) {
const status = account.banned ? '🚫 BANNED' : '❌ ERROR'
const emailShort = account.email.length > 40 ? account.email.substring(0, 37) + '...' : account.email
errorDescription += `${status} **${emailShort}**\n`
errorDescription += `• Progress: ${account.pointsEarned} pts (🖥️ ${account.desktopPoints} | 📱 ${account.mobilePoints})\n`
// Error details
if (account.banned) {
errorDescription += '• Status: Account Banned/Suspended\n'
if (account.errors?.length && account.errors[0]) {
errorDescription += `• Reason: ${account.errors[0]}\n`
}
} else if (account.errors?.length && account.errors[0]) {
errorDescription += `• Error: ${account.errors[0]}\n`
}
errorDescription += '\n'
}
errorDescription += '**📋 Recommended Actions:**\n'
errorDescription += '• Check account status manually\n'
errorDescription += '• Review error messages above\n'
errorDescription += '• Verify credentials if login failed\n'
errorDescription += '• Consider proxy rotation if rate-limited'
await ConclusionWebhook(
this.config,
'⚠️ Execution Errors & Warnings',
errorDescription,
undefined,
0xFF0000 // Red color for errors
)
} catch (error) {
log('main', 'SUMMARY', `Failed to send error report webhook: ${error instanceof Error ? error.message : String(error)}`, 'error')
}
}
/**
* Send push notification via Ntfy
*/
async sendPushNotification(summary: SummaryData): Promise<void> {
if (!this.config.ntfy?.enabled) {
return
}
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')
}
}
/**
* Update job state with completion status
*/
async updateJobState(summary: SummaryData): Promise<void> {
if (!this.jobState) {
return
}
try {
const day = summary.endTime.toISOString().split('T')?.[0]
if (!day) return
for (const account of summary.accounts) {
this.jobState.markAccountComplete(
account.email,
day,
{
totalCollected: account.pointsEarned,
banned: account.banned ?? false,
errors: account.errors?.length ?? 0
}
)
}
} catch (error) {
log('main', 'SUMMARY', `Failed to update job state: ${error instanceof Error ? error.message : String(error)}`, 'error')
}
}
/**
* Generate and send comprehensive summary
*/
async generateReport(summary: SummaryData): Promise<void> {
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')
}
log('main', 'SUMMARY', '─'.repeat(80))
log('main', 'SUMMARY', 'Account Breakdown:')
log('main', 'SUMMARY', '─'.repeat(80))
for (const account of summary.accounts) {
const status = this.hasAccountFailure(account) ? (account.banned ? '🚫 BANNED' : '❌ 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.banned) {
log('main', 'SUMMARY', ' Status: Account flagged as banned/suspended', 'error')
} else if (account.errors?.length) {
log('main', 'SUMMARY', ` Error: ${account.errors[0]}`, 'error')
}
}
log('main', 'SUMMARY', '═'.repeat(80))
// Log activity statistics
this.logActivityStats()
// Save to account history (all-time tracking)
this.saveToHistory(summary)
// Send notifications
await Promise.all([
this.sendWebhookSummary(summary),
this.sendPushNotification(summary),
this.updateJobState(summary)
])
// Reset activity stats for next run
resetActivityStatsTracker()
}
/**
* Save account results to history for all-time tracking
*/
private saveToHistory(summary: SummaryData): void {
try {
const today = new Date().toISOString().slice(0, 10)
for (const account of summary.accounts) {
// Get activity stats for this account
const tracker = getActivityStatsTracker()
const completedActivities: string[] = []
const failedActivities: string[] = []
// Extract activity completion status from tracker
tracker.getSummary().byActivity.forEach(activity => {
if (activity.successes > 0) {
completedActivities.push(activity.type)
}
if (activity.failures > 0) {
failedActivities.push(activity.type)
}
})
const entry: AccountHistoryEntry = {
timestamp: new Date().toISOString(),
date: today,
desktopPoints: account.desktopPoints,
mobilePoints: account.mobilePoints,
totalPoints: account.pointsEarned,
availablePoints: account.finalPoints,
lifetimePoints: account.finalPoints, // Approximation
dailyGoalProgress: 0, // Could extract from dashboard data
completedActivities,
failedActivities,
errors: account.errors || [],
duration: account.runDuration,
success: !this.hasAccountFailure(account)
}
this.accountHistory.addEntry(account.email, entry)
}
log('main', 'SUMMARY', `✓ Saved history for ${summary.accounts.length} account(s)`)
} catch (error) {
log('main', 'SUMMARY', `Failed to save account history: ${error instanceof Error ? error.message : String(error)}`, 'error')
}
}
/**
* Log activity success/failure statistics
*/
private logActivityStats(): void {
const tracker = getActivityStatsTracker()
const summary = tracker.getSummary()
if (summary.totalAttempts === 0) {
return
}
log('main', 'SUMMARY', '─'.repeat(80))
log('main', 'SUMMARY', '📈 Activity Statistics:')
log('main', 'SUMMARY', ` Total: ${summary.totalSuccesses}/${summary.totalAttempts} succeeded (${(summary.overallSuccessRate * 100).toFixed(1)}%)`)
// Show per-activity breakdown if there are multiple activity types
if (summary.byActivity.length > 1) {
for (const activity of summary.byActivity) {
const rate = (activity.successRate * 100).toFixed(0)
const avgTime = (activity.avgDurationMs / 1000).toFixed(1)
log('main', 'SUMMARY', ` ${activity.type}: ${activity.successes}/${activity.attempts} (${rate}%) avg ${avgTime}s`)
}
}
// Warn about problematic activities
const problematic = tracker.getProblematicActivities()
if (problematic.length > 0) {
log('main', 'SUMMARY', '⚠️ High Failure Activities:', 'warn')
for (const p of problematic) {
log('main', 'SUMMARY', ` ${p.type}: ${(p.failureRate * 100).toFixed(0)}% failure rate (${p.attempts} attempts)`, 'warn')
}
}
}
/**
* Create summary data structure from account results
*/
createSummary(
accounts: AccountResult[],
startTime: Date,
endTime: Date
): SummaryData {
const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0)
const failureCount = accounts.filter(acc => this.hasAccountFailure(acc)).length
const successCount = accounts.length - failureCount
return {
accounts,
startTime,
endTime,
totalPoints,
successCount,
failureCount
}
}
private hasAccountFailure(account: AccountResult): boolean {
return Boolean(account.errors?.length) || account.banned === true
}
}