New structure

This commit is contained in:
2025-11-11 12:59:42 +01:00
parent 088a3a024f
commit 89bc226d6b
46 changed files with 990 additions and 944 deletions

View File

@@ -0,0 +1,60 @@
/**
* Browser Factory Utility
* Eliminates code duplication between Desktop and Mobile flows
*
* Centralized browser instance creation and cleanup logic
*/
import type { BrowserContext } from 'rebrowser-playwright'
import type { MicrosoftRewardsBot } from '../../index'
import type { AccountProxy } from '../../interface/Account'
/**
* Create a browser instance for the given account
* IMPROVEMENT: Extracted from DesktopFlow and MobileFlow to eliminate duplication
*
* @param bot Bot instance
* @param proxy Account proxy configuration
* @param email Account email for session naming
* @returns Browser context ready to use
*
* @example
* const browser = await createBrowserInstance(bot, account.proxy, account.email)
*/
export async function createBrowserInstance(
bot: MicrosoftRewardsBot,
proxy: AccountProxy,
email: string
): Promise<BrowserContext> {
const browserModule = await import('../../browser/Browser')
const Browser = browserModule.default
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')
}
}

View File

@@ -0,0 +1,63 @@
import { Page } from 'rebrowser-playwright'
import type { ConfigHumanization } from '../../interface/Config'
import { Util } from '../core/Utils'
export class Humanizer {
private util: Util
private cfg: ConfigHumanization | undefined
constructor(util: Util, cfg?: ConfigHumanization) {
this.util = util
this.cfg = cfg
}
async microGestures(page: Page): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
try {
if (Math.random() < moveProb) {
const x = Math.floor(Math.random() * 40) + 5
const y = Math.floor(Math.random() * 30) + 5
await page.mouse.move(x, y, { steps: 2 }).catch(() => {
// Mouse move failed - page may be closed or unavailable
})
}
if (Math.random() < scrollProb) {
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
await page.mouse.wheel(0, dy).catch(() => {
// Mouse wheel failed - page may be closed or unavailable
})
}
} catch {
// Gesture execution failed - not critical for operation
}
}
async actionPause(): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const defMin = 150
const defMax = 450
let min = defMin
let max = defMax
if (this.cfg?.actionDelay) {
const parse = (v: number | string) => {
if (typeof v === 'number') return v
try {
const n = this.util.stringToMs(String(v))
return Math.max(0, Math.min(n, 10_000))
} catch (e) {
// Parse failed - use default minimum
return defMin
}
}
min = parse(this.cfg.actionDelay.min)
max = parse(this.cfg.actionDelay.max)
if (min > max) [min, max] = [max, min]
max = Math.min(max, 5_000)
}
await this.util.wait(this.util.randomNumber(min, max))
}
}
export default Humanizer

View File

@@ -0,0 +1,326 @@
import axios from 'axios'
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import { Architecture, ChromeVersion, EdgeVersion, Platform } from '../../interface/UserAgentUtil'
import { Retry } from '../core/Retry'
import { log } from '../notifications/Logger'
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'
const EDGE_VERSION_CACHE_TTL_MS = 1000 * 60 * 60
// Static fallback versions (updated periodically, valid as of October 2024)
const FALLBACK_EDGE_VERSIONS: EdgeVersionResult = {
android: '130.0.2849.66',
windows: '130.0.2849.68'
}
type EdgeVersionResult = {
android?: string
windows?: string
}
let edgeVersionCache: { data: EdgeVersionResult; expiresAt: number } | null = null
let edgeVersionInFlight: Promise<EdgeVersionResult> | null = null
export async function getUserAgent(isMobile: boolean): Promise<UserAgentResult> {
const system = getSystemComponents(isMobile)
const app = await getAppComponents(isMobile)
const uaTemplate = isMobile ?
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Mobile Safari/537.36 EdgA/${app.edge_version}` :
`Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${app.chrome_reduced_version} Safari/537.36 Edg/${app.edge_version}`
const platformVersion = `${isMobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0`
const uaMetadata = {
mobile: isMobile,
isMobile,
platform: isMobile ? 'Android' : 'Windows',
fullVersionList: [
{ brand: 'Not/A)Brand', version: `${NOT_A_BRAND_VERSION}.0.0.0` },
{ brand: 'Microsoft Edge', version: app['edge_version'] },
{ brand: 'Chromium', version: app['chrome_version'] }
],
brands: [
{ brand: 'Not/A)Brand', version: NOT_A_BRAND_VERSION },
{ brand: 'Microsoft Edge', version: app['edge_major_version'] },
{ brand: 'Chromium', version: app['chrome_major_version'] }
],
platformVersion,
architecture: isMobile ? '' : 'x86',
bitness: isMobile ? '' : '64',
model: '',
uaFullVersion: app['chrome_version']
}
return { userAgent: uaTemplate, userAgentMetadata: uaMetadata }
}
export async function getChromeVersion(isMobile: boolean): Promise<string> {
try {
const request = {
url: 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json',
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
const response = await axios(request)
const data: ChromeVersion = response.data
return data.channels.Stable.version
} catch (error) {
throw log(isMobile, 'USERAGENT-CHROME-VERSION', 'An error occurred:' + error, 'error')
}
}
export async function getEdgeVersions(isMobile: boolean): Promise<EdgeVersionResult> {
const now = Date.now()
// Return cached version if still valid
if (edgeVersionCache && edgeVersionCache.expiresAt > now) {
return edgeVersionCache.data
}
// Wait for in-flight request if one exists
if (edgeVersionInFlight) {
try {
return await edgeVersionInFlight
} catch (error) {
if (edgeVersionCache) {
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using cached Edge versions after in-flight failure', 'warn')
return edgeVersionCache.data
}
// Fall through to fetch attempt below
}
}
// Attempt to fetch fresh versions
const fetchPromise = fetchEdgeVersionsWithRetry(isMobile)
.then(result => {
edgeVersionCache = { data: result, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS }
edgeVersionInFlight = null
return result
})
.catch(() => {
edgeVersionInFlight = null
// Try stale cache first
if (edgeVersionCache) {
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using stale cached Edge versions due to fetch failure', 'warn')
return edgeVersionCache.data
}
// Fall back to static versions
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using static fallback Edge versions (API unavailable)', 'warn')
edgeVersionCache = { data: FALLBACK_EDGE_VERSIONS, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS }
return FALLBACK_EDGE_VERSIONS
})
edgeVersionInFlight = fetchPromise
return fetchPromise
}
export function getSystemComponents(mobile: boolean): string {
if (mobile) {
const androidVersion = 10 + Math.floor(Math.random() * 5)
return `Linux; Android ${androidVersion}; K`
}
return 'Windows NT 10.0; Win64; x64'
}
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]
const chromeVersion = await getChromeVersion(isMobile)
const chromeMajorVersion = chromeVersion?.split('.')[0]
const chromeReducedVersion = `${chromeMajorVersion}.0.0.0`
return {
not_a_brand_version: `${NOT_A_BRAND_VERSION}.0.0.0`,
not_a_brand_major_version: NOT_A_BRAND_VERSION,
edge_version: edgeVersion as string,
edge_major_version: edgeMajorVersion as string,
chrome_version: chromeVersion as string,
chrome_major_version: chromeMajorVersion as string,
chrome_reduced_version: chromeReducedVersion as string
}
}
async function fetchEdgeVersionsWithRetry(isMobile: boolean): Promise<EdgeVersionResult> {
const retry = new Retry()
return retry.run(async () => {
const versions = await fetchEdgeVersionsOnce(isMobile)
if (!versions.android && !versions.windows) {
throw new Error('Stable Edge releases did not include Android or Windows versions')
}
return versions
}, () => true)
}
async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResult> {
let lastError: unknown = null
// Try axios first
try {
const response = await axios<EdgeVersion[]>({
url: EDGE_VERSION_URL,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 10000,
validateStatus: (status) => status === 200
})
if (!response.data || !Array.isArray(response.data)) {
throw new Error('Invalid response format from Edge API')
}
return mapEdgeVersions(response.data)
} catch (axiosError) {
lastError = axiosError
// Continue to fallback
}
// Try native fetch as fallback
try {
const fallback = await tryNativeFetchFallback()
if (fallback) {
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Axios failed, using native fetch fallback', 'warn')
return fallback
}
} catch (fetchError) {
lastError = fetchError
}
// Both methods failed
const errorMsg = lastError instanceof Error ? lastError.message : String(lastError)
throw new Error(`Failed to fetch Edge versions: ${errorMsg}`)
}
async function tryNativeFetchFallback(): Promise<EdgeVersionResult | null> {
let timeoutHandle: NodeJS.Timeout | undefined
try {
const controller = new AbortController()
timeoutHandle = setTimeout(() => controller.abort(), 10000)
const response = await fetch(EDGE_VERSION_URL, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
signal: controller.signal
})
clearTimeout(timeoutHandle)
timeoutHandle = undefined
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const data = await response.json() as EdgeVersion[]
if (!Array.isArray(data)) {
throw new Error('Invalid response format')
}
return mapEdgeVersions(data)
} catch (error) {
if (timeoutHandle) clearTimeout(timeoutHandle)
return null
}
}
function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Edge API returned empty or invalid data')
}
const stable = data.find(entry => entry?.Product?.toLowerCase() === 'stable')
?? data.find(entry => entry?.Product && /stable/i.test(entry.Product))
if (!stable || !stable.Releases || !Array.isArray(stable.Releases)) {
throw new Error('Stable Edge channel not found or invalid format')
}
const androidRelease = stable.Releases.find(release =>
release?.Platform === Platform.Android && release?.ProductVersion
)
const windowsRelease = stable.Releases.find(release =>
release?.Platform === Platform.Windows &&
release?.Architecture === Architecture.X64 &&
release?.ProductVersion
) ?? stable.Releases.find(release =>
release?.Platform === Platform.Windows &&
release?.ProductVersion
)
const result: EdgeVersionResult = {
android: androidRelease?.ProductVersion,
windows: windowsRelease?.ProductVersion
}
// Validate at least one version was found
if (!result.android && !result.windows) {
throw new Error('No valid Edge versions found in API response')
}
return result
}
export async function updateFingerprintUserAgent(fingerprint: BrowserFingerprintWithHeaders, isMobile: boolean): Promise<BrowserFingerprintWithHeaders> {
try {
const userAgentData = await getUserAgent(isMobile)
const componentData = await getAppComponents(isMobile)
fingerprint.fingerprint.navigator.userAgentData = userAgentData.userAgentMetadata
fingerprint.fingerprint.navigator.userAgent = userAgentData.userAgent
fingerprint.fingerprint.navigator.appVersion = userAgentData.userAgent.replace(`${fingerprint.fingerprint.navigator.appCodeName}/`, '')
fingerprint.headers['user-agent'] = userAgentData.userAgent
fingerprint.headers['sec-ch-ua'] = `"Microsoft Edge";v="${componentData.edge_major_version}", "Not=A?Brand";v="${componentData.not_a_brand_major_version}", "Chromium";v="${componentData.chrome_major_version}"`
fingerprint.headers['sec-ch-ua-full-version-list'] = `"Microsoft Edge";v="${componentData.edge_version}", "Not=A?Brand";v="${componentData.not_a_brand_version}", "Chromium";v="${componentData.chrome_version}"`
return fingerprint
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
log(isMobile, 'USER-AGENT-UPDATE', `Failed to update fingerprint: ${errorMsg}`, 'error')
throw new Error(`User-Agent update failed: ${errorMsg}`)
}
}