feat: Refactor and modularize flow handling for improved maintainability

- Extracted BuyModeHandler, DesktopFlow, MobileFlow, and SummaryReporter into separate modules for better organization and testability.
- Enhanced type safety and added interfaces for various return types in Load, Logger, UserAgent, and flow modules.
- Implemented comprehensive error handling and logging throughout the new modules.
- Added unit tests for DesktopFlow, MobileFlow, and SummaryReporter to ensure functionality and correctness.
- Updated existing utility functions to support new flow structures and improve code clarity.
This commit is contained in:
2025-11-08 12:19:34 +01:00
parent 1ff521f505
commit 8eefd15b80
19 changed files with 1101 additions and 290 deletions

View File

@@ -7,6 +7,10 @@ import { Account } from '../interface/Account'
import { Config, ConfigBrowser, ConfigSaveFingerprint, ConfigScheduling } from '../interface/Config'
import { Util } from './Utils'
const utils = new Util()
let configCache: Config
@@ -72,11 +76,10 @@ function stripJsonComments(input: string): string {
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
function normalizeConfig(raw: unknown): Config {
// TYPE SAFETY NOTE: Using `any` here is necessary for backwards compatibility
// The config format has evolved from flat structure to nested structure over time
// We need to support both formats dynamically without knowing which one we'll receive
// Alternative approaches (discriminated unions, multiple interfaces) would require
// runtime type checking on every property access, making the code much more complex
// The validation happens implicitly through the Config interface return type
// JUSTIFIED USE OF `any`: The config format has evolved from flat nested structure over time
// This needs to support BOTH formats for backward compatibility with existing user configs
// Runtime validation happens through explicit property checks and the Config interface return type ensures type safety at function boundary
// Alternative approaches (discriminated unions, conditional types) would require extensive runtime checks making code significantly more complex
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const n = (raw || {}) as any
@@ -350,9 +353,9 @@ export function loadAccounts(): Account[] {
if (!Array.isArray(parsed)) throw new Error('accounts must be an array')
// minimal shape validation
for (const entry of parsed) {
// TYPE SAFETY NOTE: Using `any` for account validation
// Accounts come from user-provided JSON with unknown structure
// We validate each property explicitly below rather than trusting the type
// JUSTIFIED USE OF `any`: Accounts come from untrusted user JSON with unpredictable structure
// We perform explicit runtime validation of each property below (typeof checks, regex validation, etc.)
// This is safer than trusting a type assertion to a specific interface
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const a = entry as any
if (!a || typeof a.email !== 'string' || typeof a.password !== 'string') {
@@ -452,7 +455,12 @@ export function loadConfig(): Config {
}
}
export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, saveFingerprint: ConfigSaveFingerprint) {
interface SessionData {
cookies: Cookie[]
fingerprint?: BrowserFingerprintWithHeaders
}
export async function loadSessionData(sessionPath: string, email: string, isMobile: boolean, saveFingerprint: ConfigSaveFingerprint): Promise<SessionData> {
try {
// Fetch cookie file
const cookieFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_cookies' : 'desktop_cookies'}.json`)

View File

@@ -58,7 +58,7 @@ function getBuffer(url: string): WebhookBuffer {
return buf
}
async function sendBatch(url: string, buf: WebhookBuffer) {
async function sendBatch(url: string, buf: WebhookBuffer): Promise<void> {
if (buf.sending) return
buf.sending = true
while (buf.lines.length > 0) {

View File

@@ -4,7 +4,25 @@ import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import { log } from './Logger'
import { Retry } from './Retry'
import { ChromeVersion, EdgeVersion, Architecture, Platform } from '../interface/UserAgentUtil'
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../interface/UserAgentUtil'
interface UserAgentMetadata {
mobile: boolean
isMobile: boolean
platform: string
fullVersionList: Array<{ brand: string; version: string }>
brands: Array<{ brand: string; version: string }>
platformVersion: string
architecture: string
bitness: string
model: string
uaFullVersion: string
}
interface UserAgentResult {
userAgent: string
userAgentMetadata: UserAgentMetadata
}
const NOT_A_BRAND_VERSION = '99'
const EDGE_VERSION_URL = 'https://edgeupdates.microsoft.com/api/products'
@@ -24,7 +42,7 @@ type EdgeVersionResult = {
let edgeVersionCache: { data: EdgeVersionResult; expiresAt: number } | null = null
let edgeVersionInFlight: Promise<EdgeVersionResult> | null = null
export async function getUserAgent(isMobile: boolean) {
export async function getUserAgent(isMobile: boolean): Promise<UserAgentResult> {
const system = getSystemComponents(isMobile)
const app = await getAppComponents(isMobile)
@@ -133,7 +151,17 @@ export function getSystemComponents(mobile: boolean): string {
return 'Windows NT 10.0; Win64; x64'
}
export async function getAppComponents(isMobile: boolean) {
interface AppComponents {
not_a_brand_version: string
not_a_brand_major_version: string
edge_version: string
edge_major_version: string
chrome_version: string
chrome_major_version: string
chrome_reduced_version: string
}
export async function getAppComponents(isMobile: boolean): Promise<AppComponents> {
const versions = await getEdgeVersions(isMobile)
const edgeVersion = isMobile ? versions.android : versions.windows as string
const edgeMajorVersion = edgeVersion?.split('.')[0]