mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 09:46:16 +00:00
feat: Enhance Microsoft Rewards Bot with recovery email normalization and improved logging
- Added `normalizeRecoveryEmail` utility function for consistent recovery email validation. - Improved logging functionality in `Logger.ts` with enhanced edge case handling and null checks. - Centralized browser cleanup logic in `BrowserFactory.ts` to eliminate duplication. - Refactored error handling and message formatting in `Utils.ts` for better clarity and consistency. - Updated various log messages for improved readability and consistency across the codebase. - Implemented periodic cleanup of webhook buffers in `Logger.ts` using centralized constants.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Browser Factory Utility
|
||||
* Eliminates code duplication between Desktop and Mobile flows
|
||||
*
|
||||
* Centralized browser instance creation logic
|
||||
* Centralized browser instance creation and cleanup logic
|
||||
*/
|
||||
|
||||
import type { BrowserContext } from 'rebrowser-playwright'
|
||||
@@ -22,8 +22,8 @@ import type { AccountProxy } from '../interface/Account'
|
||||
* const browser = await createBrowserInstance(bot, account.proxy, account.email)
|
||||
*/
|
||||
export async function createBrowserInstance(
|
||||
bot: MicrosoftRewardsBot,
|
||||
proxy: AccountProxy,
|
||||
bot: MicrosoftRewardsBot,
|
||||
proxy: AccountProxy,
|
||||
email: string
|
||||
): Promise<BrowserContext> {
|
||||
const browserModule = await import('../browser/Browser')
|
||||
@@ -31,3 +31,30 @@ export async function createBrowserInstance(
|
||||
const browserInstance = new Browser(bot)
|
||||
return await browserInstance.createBrowser(proxy, email)
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely close browser context with error handling
|
||||
* IMPROVEMENT: Extracted from DesktopFlow and MobileFlow to eliminate duplication
|
||||
*
|
||||
* @param bot Bot instance
|
||||
* @param browser Browser context to close
|
||||
* @param email Account email for logging
|
||||
* @param isMobile Whether this is a mobile browser context
|
||||
*
|
||||
* @example
|
||||
* await closeBrowserSafely(bot, browser, account.email, false)
|
||||
*/
|
||||
export async function closeBrowserSafely(
|
||||
bot: MicrosoftRewardsBot,
|
||||
browser: BrowserContext,
|
||||
email: string,
|
||||
isMobile: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
await bot.browser.func.closeBrowser(browser, email)
|
||||
} catch (closeError) {
|
||||
const message = closeError instanceof Error ? closeError.message : String(closeError)
|
||||
const platform = isMobile ? 'mobile' : 'desktop'
|
||||
bot.log(isMobile, `${platform.toUpperCase()}-FLOW`, `Failed to close ${platform} context: ${message}`, 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import { DISCORD, TIMEOUTS } from '../constants'
|
||||
import { DISCORD, LOGGER_CLEANUP } from '../constants'
|
||||
import { sendErrorReport } from './ErrorReportingWebhook'
|
||||
import { loadConfig } from './Load'
|
||||
import { Ntfy } from './Ntfy'
|
||||
@@ -26,22 +26,19 @@ type WebhookBuffer = {
|
||||
const webhookBuffers = new Map<string, WebhookBuffer>()
|
||||
|
||||
// Periodic cleanup of old/idle webhook buffers to prevent memory leaks
|
||||
// IMPROVED: Using centralized constants instead of magic numbers
|
||||
const BUFFER_MAX_AGE_MS = TIMEOUTS.ONE_HOUR
|
||||
const BUFFER_CLEANUP_INTERVAL_MS = TIMEOUTS.TEN_MINUTES
|
||||
|
||||
// IMPROVED: Using centralized constants from constants.ts
|
||||
const cleanupInterval = setInterval(() => {
|
||||
const now = Date.now()
|
||||
|
||||
|
||||
for (const [url, buf] of webhookBuffers.entries()) {
|
||||
if (!buf.sending && buf.lines.length === 0) {
|
||||
const lastActivity = (buf as unknown as { lastActivity?: number }).lastActivity || 0
|
||||
if (now - lastActivity > BUFFER_MAX_AGE_MS) {
|
||||
if (now - lastActivity > LOGGER_CLEANUP.BUFFER_MAX_AGE_MS) {
|
||||
webhookBuffers.delete(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, BUFFER_CLEANUP_INTERVAL_MS)
|
||||
}, LOGGER_CLEANUP.BUFFER_CLEANUP_INTERVAL_MS)
|
||||
|
||||
// FIXED: Allow cleanup to be stopped with proper fallback
|
||||
// unref() prevents process from hanging but may not exist in all environments
|
||||
@@ -134,7 +131,7 @@ const COLOR_RULES: ColorRule[] = [
|
||||
|
||||
function determineColorFromContent(content: string): number {
|
||||
const lower = content.toLowerCase()
|
||||
|
||||
|
||||
// Check rules in priority order
|
||||
for (const rule of COLOR_RULES) {
|
||||
if (typeof rule.pattern === 'string') {
|
||||
@@ -143,19 +140,50 @@ function determineColorFromContent(content: string): number {
|
||||
if (rule.pattern.test(lower)) return rule.color
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return DISCORD.COLOR_GRAY
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if config has valid logging configuration
|
||||
* IMPROVED: Enhanced edge case handling and null checks
|
||||
*/
|
||||
function hasValidLogging(config: unknown): config is { logging: { excludeFunc?: string[]; webhookExcludeFunc?: string[] } } {
|
||||
return typeof config === 'object' &&
|
||||
config !== null &&
|
||||
'logging' in config &&
|
||||
typeof config.logging === 'object' &&
|
||||
config.logging !== null
|
||||
function hasValidLogging(config: unknown): config is { logging: { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean; liveWebhookUrl?: string } } {
|
||||
if (typeof config !== 'object' || config === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!('logging' in config)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const cfg = config as Record<string, unknown>
|
||||
const logging = cfg.logging
|
||||
|
||||
if (typeof logging !== 'object' || logging === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate optional fields have correct types if present
|
||||
const loggingObj = logging as Record<string, unknown>
|
||||
|
||||
if ('excludeFunc' in loggingObj && !Array.isArray(loggingObj.excludeFunc)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ('webhookExcludeFunc' in loggingObj && !Array.isArray(loggingObj.webhookExcludeFunc)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ('redactEmails' in loggingObj && typeof loggingObj.redactEmails !== 'boolean') {
|
||||
return false
|
||||
}
|
||||
|
||||
if ('liveWebhookUrl' in loggingObj && typeof loggingObj.liveWebhookUrl !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function enqueueWebhookLog(url: string, line: string) {
|
||||
@@ -193,13 +221,13 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
|
||||
const currentTime = new Date().toLocaleString()
|
||||
const platformText = isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP'
|
||||
|
||||
|
||||
// Clean string for notifications (no chalk, structured)
|
||||
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
|
||||
const loggingCfg: LoggingCfg = logging || {}
|
||||
const shouldRedact = !!loggingCfg.redactEmails
|
||||
const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => {
|
||||
const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}`
|
||||
const [u, d] = m.split('@'); return `${(u || '').slice(0, 2)}***@${d || ''}`
|
||||
}) : s
|
||||
const cleanStr = redact(`[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`)
|
||||
|
||||
@@ -210,7 +238,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
message.toLowerCase().includes('press the number'),
|
||||
message.toLowerCase().includes('no points to earn')
|
||||
],
|
||||
error: [],
|
||||
error: [],
|
||||
warn: [
|
||||
message.toLowerCase().includes('aborting'),
|
||||
message.toLowerCase().includes('didn\'t gain')
|
||||
@@ -229,11 +257,11 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : '✓'
|
||||
const platformColor = isMobile === 'main' ? chalk.cyan : isMobile ? chalk.blue : chalk.magenta
|
||||
const typeColor = type === 'error' ? chalk.red : type === 'warn' ? chalk.yellow : chalk.green
|
||||
|
||||
|
||||
// Add contextual icon based on title/message (ASCII-safe for Windows PowerShell)
|
||||
const titleLower = title.toLowerCase()
|
||||
const msgLower = message.toLowerCase()
|
||||
|
||||
|
||||
// ASCII-safe icons for Windows PowerShell compatibility
|
||||
const iconMap: Array<[RegExp, string]> = [
|
||||
[/security|compromised/i, '[SECURITY]'],
|
||||
@@ -248,7 +276,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
[/browser/i, '[BROWSER]'],
|
||||
[/main/i, '[MAIN]']
|
||||
]
|
||||
|
||||
|
||||
let icon = ''
|
||||
for (const [pattern, symbol] of iconMap) {
|
||||
if (pattern.test(titleLower) || pattern.test(msgLower)) {
|
||||
@@ -256,9 +284,9 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const iconPart = icon ? icon + ' ' : ''
|
||||
|
||||
|
||||
const formattedStr = [
|
||||
chalk.gray(`[${currentTime}]`),
|
||||
chalk.gray(`[${process.pid}]`),
|
||||
@@ -304,7 +332,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
// Automatic error reporting to community webhook (fire and forget)
|
||||
if (type === 'error') {
|
||||
const errorObj = new Error(cleanStr)
|
||||
|
||||
|
||||
// Send error report asynchronously without blocking
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
@@ -318,7 +346,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
}).catch(() => {
|
||||
// Catch any promise rejection silently
|
||||
})
|
||||
|
||||
|
||||
return errorObj
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import ms from 'ms'
|
||||
* @returns String representation of the error
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,8 +23,8 @@ export function getErrorMessage(error: unknown): string {
|
||||
* formatErrorMessage('LOGIN', err, 'Failed') // 'Failed in LOGIN: Invalid credentials'
|
||||
*/
|
||||
export function formatErrorMessage(context: string, error: unknown, prefix: string = 'Error'): string {
|
||||
const errorMsg = getErrorMessage(error)
|
||||
return `${prefix} in ${context}: ${errorMsg}`
|
||||
const errorMsg = getErrorMessage(error)
|
||||
return `${prefix} in ${context}: ${errorMsg}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,21 +35,25 @@ export class Util {
|
||||
|
||||
/**
|
||||
* Wait for a specified number of milliseconds
|
||||
* @param ms - Milliseconds to wait (max 1 hour)
|
||||
* @throws {Error} If ms is not finite or is NaN/Infinity
|
||||
* @param ms - Milliseconds to wait (max 1 hour, min 0)
|
||||
* @throws {Error} If ms is not finite, is NaN/Infinity, or is negative
|
||||
* @example await utils.wait(1000) // Wait 1 second
|
||||
*/
|
||||
wait(ms: number): Promise<void> {
|
||||
const MAX_WAIT_MS = 3600000 // 1 hour max to prevent infinite waits
|
||||
const MIN_WAIT_MS = 0
|
||||
|
||||
// FIXED: Simplified validation - isFinite checks both NaN and Infinity
|
||||
|
||||
// FIXED: Comprehensive validation - check finite, NaN, Infinity, and negative values
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error(`Invalid wait time: ${ms}. Must be a finite number (not NaN or Infinity).`)
|
||||
}
|
||||
|
||||
|
||||
if (ms < 0) {
|
||||
throw new Error(`Invalid wait time: ${ms}. Cannot wait negative milliseconds.`)
|
||||
}
|
||||
|
||||
const safeMs = Math.min(Math.max(MIN_WAIT_MS, ms), MAX_WAIT_MS)
|
||||
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, safeMs)
|
||||
})
|
||||
@@ -66,11 +70,11 @@ export class Util {
|
||||
if (!Number.isFinite(minMs) || !Number.isFinite(maxMs)) {
|
||||
throw new Error(`Invalid wait range: min=${minMs}, max=${maxMs}. Both must be finite numbers.`)
|
||||
}
|
||||
|
||||
|
||||
if (minMs > maxMs) {
|
||||
throw new Error(`Invalid wait range: min (${minMs}) cannot be greater than max (${maxMs}).`)
|
||||
}
|
||||
|
||||
|
||||
const delta = this.randomNumber(minMs, maxMs)
|
||||
return this.wait(delta)
|
||||
}
|
||||
@@ -115,11 +119,11 @@ export class Util {
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
throw new Error(`Invalid range: min=${min}, max=${max}. Both must be finite numbers.`)
|
||||
}
|
||||
|
||||
|
||||
if (min > max) {
|
||||
throw new Error(`Invalid range: min (${min}) cannot be greater than max (${max}).`)
|
||||
}
|
||||
|
||||
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
@@ -136,19 +140,19 @@ export class Util {
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new Error('Invalid input: arr must be an array.')
|
||||
}
|
||||
|
||||
|
||||
if (arr.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
if (!Number.isFinite(numChunks) || numChunks <= 0) {
|
||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be a positive finite number.`)
|
||||
}
|
||||
|
||||
|
||||
if (!Number.isInteger(numChunks)) {
|
||||
throw new Error(`Invalid numChunks: ${numChunks}. Must be an integer.`)
|
||||
}
|
||||
|
||||
|
||||
const safeNumChunks = Math.max(1, Math.floor(numChunks))
|
||||
const chunkSize = Math.ceil(arr.length / safeNumChunks)
|
||||
const chunks: T[][] = []
|
||||
@@ -174,7 +178,7 @@ export class Util {
|
||||
if (typeof input !== 'string' && typeof input !== 'number') {
|
||||
throw new Error('Invalid input type. Expected string or number.')
|
||||
}
|
||||
|
||||
|
||||
const milisec = ms(input.toString())
|
||||
if (!milisec || !Number.isFinite(milisec)) {
|
||||
throw new Error('The string provided cannot be parsed to a valid time! Use a format like "1 min", "1m" or "1 minutes"')
|
||||
@@ -214,4 +218,25 @@ export function formatDetailedError(label: string, error: unknown, includeStack:
|
||||
return `${label}:${baseMessage} :: ${stackLines}`
|
||||
}
|
||||
return `${label}:${baseMessage}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize recovery email
|
||||
* IMPROVEMENT: Extracted to eliminate duplication and provide consistent validation
|
||||
*
|
||||
* @param recoveryEmail - Raw recovery email value from account configuration
|
||||
* @returns Normalized recovery email string or undefined if invalid
|
||||
*
|
||||
* @example
|
||||
* normalizeRecoveryEmail(' test@example.com ') // 'test@example.com'
|
||||
* normalizeRecoveryEmail('') // undefined
|
||||
* normalizeRecoveryEmail(undefined) // undefined
|
||||
*/
|
||||
export function normalizeRecoveryEmail(recoveryEmail: unknown): string | undefined {
|
||||
if (typeof recoveryEmail !== 'string') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const trimmed = recoveryEmail.trim()
|
||||
return trimmed === '' ? undefined : trimmed
|
||||
}
|
||||
Reference in New Issue
Block a user