UNPKG

@fanboynz/network-scanner

Version:

A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.

1,204 lines (1,095 loc) 45.8 kB
/** * Enhanced Mouse Interaction and Page Simulation Module * ==================================================== * * This module provides sophisticated, human-like interaction simulation for web scraping * and automation tasks. It replaces basic mouse movements with realistic behavior patterns * that are harder to detect by anti-bot systems. * * KEY FEATURES: * - Human-like mouse movements with curves and jitter * - Realistic scrolling simulation with smooth increments * - Safe element interaction (avoids destructive actions) * - Typing simulation with mistakes and variable timing * - Configurable intensity levels (low/medium/high) * - Site-specific optimization based on URL patterns * * USAGE EXAMPLES: * * Basic interaction: * await performPageInteraction(page, url, {}, debug); * * Custom configuration: * const config = createInteractionConfig(url, siteConfig); * await performPageInteraction(page, url, config, debug); * * Manual mouse movement: * await humanLikeMouseMove(page, 0, 0, 500, 300, { * steps: 20, curve: 0.5, jitter: 3 * }); * * CONFIGURATION OPTIONS: * - intensity: 'low' | 'medium' | 'high' - Overall interaction intensity * - duration: number - Total interaction time in milliseconds * - mouseMovements: number - Number of mouse movements to perform * - includeScrolling: boolean - Enable scrolling simulation * - includeElementClicks: boolean - Enable safe element clicking * - includeTyping: boolean - Enable typing simulation * * ANTI-DETECTION FEATURES: * - Variable timing between actions * - Curved mouse movements (not straight lines) * - Random jitter and pauses * - Site-specific behavior patterns * - Realistic scrolling with momentum * - Human-like typing with occasional mistakes * * SAFETY FEATURES: * - Avoids clicking destructive elements (delete, buy, submit) * - Bounded coordinate generation (stays within viewport) * - Graceful error handling (failures don't break main scan) * - Optional element interaction (disabled by default) * * @version 1.0 * @requires puppeteer */ // Fast setTimeout helper for Puppeteer 22.x compatibility // Uses standard Promise constructor for better performance than node:timers/promises function fastTimeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // === VIEWPORT AND COORDINATE CONSTANTS === // These control the default viewport assumptions and coordinate generation const DEFAULT_VIEWPORT = { WIDTH: 1200, // Default viewport width if not detected HEIGHT: 800 // Default viewport height if not detected }; const COORDINATE_MARGINS = { DEFAULT_X: 50, // Minimum distance from left/right edges DEFAULT_Y: 50, // Minimum distance from top/bottom edges EDGE_ZONE_SIZE: 200, // Size of "edge" zones for preferEdges mode CENTER_AVOID_RATIO: 0.25 // Percentage of viewport to avoid in center (0.25 = 25%) }; // === MOUSE MOVEMENT CONSTANTS === // Fine-tune mouse movement behavior for realism vs. speed const MOUSE_MOVEMENT = { MIN_STEPS: 5, // Minimum steps for any movement DEFAULT_STEPS: 15, // Default steps for mouse movement MAX_STEPS: 30, // Maximum steps to prevent excessive slowness MIN_DELAY: 5, // Minimum milliseconds between movement steps MAX_DELAY: 25, // Maximum milliseconds between movement steps DEFAULT_CURVE: 0.2, // Default curve intensity (reduced for performance) DEFAULT_JITTER: 2, // Default random jitter in pixels DISTANCE_STEP_RATIO: 200, // CRITICAL: 4x increase to drastically reduce steps CURVE_INTENSITY_RATIO: 0.01 // Multiplier for curve calculation }; // === SCROLLING CONSTANTS === // Control scrolling behavior - adjust for different site types const SCROLLING = { DEFAULT_AMOUNT: 3, // Default number of scroll actions DEFAULT_SMOOTHNESS: 5, // Default smoothness (higher = more increments) SCROLL_DELTA: 200, // Pixels to scroll per action PAUSE_BETWEEN: 50, // Milliseconds between scroll actions SMOOTH_INCREMENT_DELAY: 20 // Milliseconds between smooth scroll increments }; // === INTERACTION TIMING CONSTANTS === // All timing values in milliseconds - adjust for faster/slower interaction const TIMING = { CLICK_PAUSE_MIN: 100, // Minimum pause before clicking CLICK_PAUSE_MAX: 200, // Maximum pause before clicking POST_CLICK_MIN: 300, // Minimum pause after clicking POST_CLICK_MAX: 500, // Maximum pause after clicking TYPING_MIN_DELAY: 50, // Minimum delay between keystrokes TYPING_MAX_DELAY: 150, // Maximum delay between keystrokes MISTAKE_PAUSE_MIN: 100, // Minimum pause after typing mistake MISTAKE_PAUSE_MAX: 200, // Maximum pause after typing mistake BACKSPACE_DELAY_MIN: 50, // Minimum delay before backspace BACKSPACE_DELAY_MAX: 100, // Maximum delay before backspace DEFAULT_INTERACTION_DURATION: 2000 // Default total interaction time }; // === ELEMENT INTERACTION CONSTANTS === // Safety and behavior settings for element interaction const ELEMENT_INTERACTION = { MAX_ATTEMPTS: 3, // Maximum attempts to find clickable elements TIMEOUT: 2000, // Timeout for element operations TEXT_PREVIEW_LENGTH: 50, // Characters to capture for element text preview MISTAKE_RATE: 0.02 // Probability of typing mistakes (0.02 = 2% chance) }; // === CONTENT CLICK CONSTANTS === // For triggering document-level onclick handlers (e.g., Monetag onclick_static) // These clicks target the page content area, not specific UI elements // NOTE: No preDelay needed — mouse movements + scrolling already provide ~1s // of activity before clicks fire, which is enough for async ad script registration const CONTENT_CLICK = { CLICK_COUNT: 2, // Two attempts (primary + backup if first suppressed) INTER_CLICK_MIN: 300, // Minimum ms between clicks (above Monetag 250ms cooldown) INTER_CLICK_MAX: 500, // Maximum ms between clicks PRE_CLICK_DELAY: 300, // Small buffer for late-loading async ad scripts VIEWPORT_INSET: 0.2, // Avoid outer 20% of viewport (menus, overlays) MOUSE_APPROACH_STEPS: 3 // Minimal steps — just enough for non-instant movement }; // === INTENSITY SETTINGS === // Pre-configured intensity levels - modify these to change overall behavior const INTENSITY_SETTINGS = { LOW: { movements: 2, // Fewer movements for minimal interaction scrolls: 1, // Minimal scrolling pauseMultiplier: 1.5 // 50% longer pauses }, MEDIUM: { movements: 3, // Balanced movement count scrolls: 2, // Moderate scrolling pauseMultiplier: 1.0 // Normal timing }, HIGH: { movements: 5, // More movements for thorough interaction scrolls: 3, // More scrolling activity pauseMultiplier: 0.7 // 30% shorter pauses for faster interaction } }; // === SITE-SPECIFIC DURATION CONSTANTS === // Different interaction durations based on site type const SITE_DURATIONS = { NEWS_BLOG: 3000, // Longer duration for content-heavy sites SOCIAL_FORUM: 2500, // Medium duration for social platforms DEFAULT: 2000 // Standard duration for most sites }; // === PROBABILITY CONSTANTS === // Control randomness and behavior patterns const PROBABILITIES = { PAUSE_CHANCE: 0.3, // 30% chance of random pause during movement SCROLL_DOWN_BIAS: 0.7, // 70% chance to scroll down (vs up) EDGE_PREFERENCE: { // Probabilities for edge selection in preferEdges mode LEFT: 0.25, // 0-25% = left edge RIGHT: 0.5, // 25-50% = right edge TOP: 0.75, // 50-75% = top edge BOTTOM: 1.0 // 75-100% = bottom edge } }; // === PERFORMANCE OPTIMIZATION VARIABLES === // Viewport caching to reduce repeated page.viewport() calls let cachedViewport = null; let lastViewportCheck = 0; const VIEWPORT_CACHE_DURATION = 30000; // 30 seconds let interactionMemoryCleanupCounter = 0; /** * Gets viewport dimensions with caching for performance * Caches viewport for 30 seconds to avoid repeated queries * * @param {import('puppeteer').Page} page - Puppeteer page object * @returns {Promise<object>} Viewport dimensions {width, height} */ async function getCachedViewport(page) { const now = Date.now(); // Return cached viewport if still valid if (cachedViewport && (now - lastViewportCheck) < VIEWPORT_CACHE_DURATION) { return cachedViewport; } try { cachedViewport = await page.viewport(); lastViewportCheck = now; return cachedViewport || { width: DEFAULT_VIEWPORT.WIDTH, height: DEFAULT_VIEWPORT.HEIGHT }; } catch (viewportErr) { // Return defaults if viewport query fails return { width: DEFAULT_VIEWPORT.WIDTH, height: DEFAULT_VIEWPORT.HEIGHT }; } } /** * Generates random coordinates within viewport bounds with intelligent placement * * COORDINATE GENERATION MODES: * - Normal: Random coordinates within margins * - preferEdges: Bias towards viewport edges (more realistic) * - avoidCenter: Exclude center area (useful for ads/popups) * * DEVELOPER NOTES: * - Always respects marginX/marginY to prevent edge clipping * - Edge zones are 200px from each edge by default * - Center avoidance creates a circular exclusion zone * - Returns {x, y} object with integer coordinates * * @param {number} maxX - Maximum X coordinate (viewport width) * @param {number} maxY - Maximum Y coordinate (viewport height) * @param {object} options - Configuration options * @param {number} options.marginX - Minimum distance from left/right edges * @param {number} options.marginY - Minimum distance from top/bottom edges * @param {boolean} options.avoidCenter - Exclude center area (25% of viewport) * @param {boolean} options.preferEdges - Bias coordinates towards edges * @returns {object} Generated coordinates {x, y} * * @example * // Basic random coordinates * const pos = generateRandomCoordinates(1920, 1080); * * // Prefer edges for more natural movement * const edgePos = generateRandomCoordinates(1920, 1080, { preferEdges: true }); * * // Avoid center area (useful for avoiding ads) * const safePos = generateRandomCoordinates(1920, 1080, { avoidCenter: true }); */ function generateRandomCoordinates(maxX = DEFAULT_VIEWPORT.WIDTH, maxY = DEFAULT_VIEWPORT.HEIGHT, options = {}) { const { marginX = COORDINATE_MARGINS.DEFAULT_X, marginY = COORDINATE_MARGINS.DEFAULT_Y, avoidCenter = false, preferEdges = false } = options; let x, y; if (preferEdges) { // Prefer coordinates near edges for more realistic behavior const edge = Math.random(); if (edge < PROBABILITIES.EDGE_PREFERENCE.LEFT) { // Left edge x = Math.floor(Math.random() * COORDINATE_MARGINS.EDGE_ZONE_SIZE) + marginX; y = Math.floor(Math.random() * (maxY - 2 * marginY)) + marginY; } else if (edge < PROBABILITIES.EDGE_PREFERENCE.RIGHT) { // Right edge x = Math.floor(Math.random() * COORDINATE_MARGINS.EDGE_ZONE_SIZE) + (maxX - COORDINATE_MARGINS.EDGE_ZONE_SIZE - marginX); y = Math.floor(Math.random() * (maxY - 2 * marginY)) + marginY; } else if (edge < PROBABILITIES.EDGE_PREFERENCE.TOP) { // Top edge x = Math.floor(Math.random() * (maxX - 2 * marginX)) + marginX; y = Math.floor(Math.random() * COORDINATE_MARGINS.EDGE_ZONE_SIZE) + marginY; } else { // Bottom edge x = Math.floor(Math.random() * (maxX - 2 * marginX)) + marginX; y = Math.floor(Math.random() * COORDINATE_MARGINS.EDGE_ZONE_SIZE) + (maxY - COORDINATE_MARGINS.EDGE_ZONE_SIZE - marginY); } } else if (avoidCenter) { // Avoid center area const centerX = maxX / 2; const centerY = maxY / 2; const avoidRadius = Math.min(maxX, maxY) * COORDINATE_MARGINS.CENTER_AVOID_RATIO; do { x = Math.floor(Math.random() * (maxX - 2 * marginX)) + marginX; y = Math.floor(Math.random() * (maxY - 2 * marginY)) + marginY; } while (Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2) < avoidRadius); } else { // Standard random coordinates x = Math.floor(Math.random() * (maxX - 2 * marginX)) + marginX; y = Math.floor(Math.random() * (maxY - 2 * marginY)) + marginY; } return { x, y }; } /** * Simulates human-like mouse movement with realistic timing and curves * * MOVEMENT CHARACTERISTICS: * - Uses easing curves (slow start, fast middle, slow end) * - Adds slight curve to path (not straight lines) * - Random jitter for micro-movements * - Variable timing between steps * - Automatically calculates optimal step count based on distance * * PERFORMANCE NOTES: * - Longer distances automatically use more steps * - Very short movements use minimum steps to prevent slowness * - Maximum steps cap prevents excessive delays * * ANTI-DETECTION FEATURES: * - No perfectly straight lines * - Realistic acceleration/deceleration * - Micro-movements simulate hand tremor * - Variable timing prevents pattern detection * * @param {import('puppeteer').Page} page - Puppeteer page object * @param {number} fromX - Starting X coordinate * @param {number} fromY - Starting Y coordinate * @param {number} toX - Target X coordinate * @param {number} toY - Target Y coordinate * @param {object} options - Movement configuration * @param {number} options.steps - Number of movement steps (auto-calculated if not specified) * @param {number} options.minDelay - Minimum delay between steps in ms * @param {number} options.maxDelay - Maximum delay between steps in ms * @param {number} options.curve - Curve intensity (0.0 = straight, 1.0 = very curved) * @param {number} options.jitter - Random jitter amount in pixels * * @example * // Basic movement * await humanLikeMouseMove(page, 0, 0, 500, 300); * * // Slow, very curved movement * await humanLikeMouseMove(page, 0, 0, 500, 300, { * steps: 25, curve: 0.8, minDelay: 20, maxDelay: 50 * }); * * // Fast, minimal curve movement * await humanLikeMouseMove(page, 0, 0, 500, 300, { * steps: 8, curve: 0.1, minDelay: 2, maxDelay: 8 * }); */ async function humanLikeMouseMove(page, fromX, fromY, toX, toY, options = {}) { const { steps = MOUSE_MOVEMENT.DEFAULT_STEPS, minDelay = MOUSE_MOVEMENT.MIN_DELAY, maxDelay = MOUSE_MOVEMENT.MAX_DELAY, curve = MOUSE_MOVEMENT.DEFAULT_CURVE, jitter = MOUSE_MOVEMENT.DEFAULT_JITTER } = options; const distance = Math.sqrt((toX - fromX) ** 2 + (toY - fromY) ** 2); // FIXED: More aggressive step capping to prevent excessive delays let actualSteps; if (options.steps) { // CRITICAL: Much lower step cap to prevent timeouts actualSteps = Math.min(options.steps, 8); // Was MAX_STEPS (30), now max 8 } else { // Calculate steps based on distance with strict limits const calculatedSteps = Math.floor(distance / MOUSE_MOVEMENT.DISTANCE_STEP_RATIO); actualSteps = Math.max( 2, // Min 2 steps instead of 5 Math.min(calculatedSteps, 6) // Max 6 steps instead of 30 ); } // CRITICAL: Emergency timeout - never exceed 300ms for mouse movement const maxTotalTime = 300; // 300ms maximum (was 2000ms) const estimatedTime = actualSteps * maxDelay; if (estimatedTime > maxTotalTime) { actualSteps = Math.floor(maxTotalTime / maxDelay); // Optional: Log performance cap without forceDebug dependency // console.log(`[interaction] Capped steps to ${actualSteps} to prevent timeout (estimated: ${estimatedTime}ms)`); } for (let i = 0; i <= actualSteps; i++) { // Bail out if page closed mid-movement try { if (page.isClosed()) return; } catch { return; } const progress = i / actualSteps; // Apply easing curve for more natural movement const easedProgress = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2; // Calculate base position let currentX = fromX + (toX - fromX) * easedProgress; let currentY = fromY + (toY - fromY) * easedProgress; // Add slight curve to movement (more human-like) if (curve > 0 && i > 0 && i < actualSteps) { const curveIntensity = Math.sin((i / actualSteps) * Math.PI) * curve * distance * MOUSE_MOVEMENT.CURVE_INTENSITY_RATIO; const perpX = -(toY - fromY) / distance; const perpY = (toX - fromX) / distance; currentX += perpX * curveIntensity; currentY += perpY * curveIntensity; } // Add small random jitter for realism if (jitter > 0 && i > 0 && i < actualSteps) { currentX += (Math.random() - 0.5) * jitter; currentY += (Math.random() - 0.5) * jitter; } await page.mouse.move(currentX, currentY); // Variable delay between movements if (i < actualSteps) { const delay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; await fastTimeout(delay); } } } /** * Simulates realistic scrolling behavior with momentum and smoothness * * SCROLLING FEATURES: * - Smooth scrolling broken into increments (not instant jumps) * - Configurable direction (up/down) * - Variable scroll amounts and speeds * - Pauses between scroll actions for realism * * DEVELOPER NOTES: * - Uses page.mouse.wheel() for browser-native scrolling * - Smoothness parameter controls increment count (higher = smoother) * - Each scroll action is split into multiple small increments * - Automatically handles scroll failures gracefully * * @param {import('puppeteer').Page} page - Puppeteer page object * @param {object} options - Scrolling configuration * @param {string} options.direction - 'down' or 'up' * @param {number} options.amount - Number of scroll actions to perform * @param {number} options.smoothness - Smoothness level (1-10, higher = smoother) * @param {number} options.pauseBetween - Milliseconds pause between scroll actions * * @example * // Basic downward scrolling * await simulateScrolling(page); * * // Smooth upward scrolling * await simulateScrolling(page, { * direction: 'up', amount: 5, smoothness: 8 * }); * * // Fast scrolling with minimal smoothness * await simulateScrolling(page, { * direction: 'down', amount: 2, smoothness: 2, pauseBetween: 100 * }); */ async function simulateScrolling(page, options = {}) { const { direction = 'down', amount = SCROLLING.DEFAULT_AMOUNT, smoothness = SCROLLING.DEFAULT_SMOOTHNESS, pauseBetween = SCROLLING.PAUSE_BETWEEN } = options; try { for (let i = 0; i < amount; i++) { try { if (page.isClosed()) return; } catch { return; } const scrollDelta = direction === 'down' ? SCROLLING.SCROLL_DELTA : -SCROLLING.SCROLL_DELTA; // Smooth scrolling by breaking into smaller increments for (let j = 0; j < smoothness; j++) { await page.mouse.wheel({ deltaY: scrollDelta / smoothness }); await fastTimeout(SCROLLING.SMOOTH_INCREMENT_DELAY); } if (i < amount - 1) { await fastTimeout(pauseBetween); } } } catch (scrollErr) { // Silently handle scroll errors - not critical for functionality } } /** * Attempts to find and interact with clickable elements safely * * SAFETY FEATURES: * - Avoids destructive actions (delete, buy, submit buttons) * - Only interacts with visible, clickable elements * - Bounded to viewport coordinates * - Graceful failure handling * * ELEMENT DETECTION: * - Searches for buttons, links, and role="button" elements * - Filters by visibility (width/height > 0, within viewport) * - Text-based filtering to avoid dangerous actions * - Random selection from available safe elements * * INTERACTION FLOW: * 1. Find all matching elements in viewport * 2. Filter out dangerous elements by text content * 3. Randomly select one element * 4. Move mouse to element center * 5. Pause briefly, then click * 6. Pause after clicking * * DEVELOPER NOTES: * - Set avoidDestructive: false to disable safety filtering * - Customize elementTypes to target specific element types * - maxAttempts controls retry behavior * - All errors are caught to prevent breaking main scan * * @param {import('puppeteer').Page} page - Puppeteer page object * @param {object} options - Element interaction configuration * @param {number} options.maxAttempts - Maximum attempts to find elements * @param {string[]} options.elementTypes - CSS selectors for clickable elements * @param {boolean} options.avoidDestructive - Avoid dangerous actions * @param {number} options.timeout - Timeout for element operations * * @example * // Safe element interaction (default) * await interactWithElements(page); * * // Custom element types * await interactWithElements(page, { * elementTypes: ['button', '.custom-button', '#specific-id'], * maxAttempts: 5 * }); * * // Allow all interactions (dangerous!) * await interactWithElements(page, { * avoidDestructive: false, * elementTypes: ['button', 'input[type="submit"]'] * }); */ async function interactWithElements(page, options = {}) { const { maxAttempts = ELEMENT_INTERACTION.MAX_ATTEMPTS, elementTypes = ['button', 'a', '[role="button"]'], avoidDestructive = true, timeout = ELEMENT_INTERACTION.TIMEOUT } = options; try { // Ensure page is in valid state for element interaction try { // Check if page is closed before attempting interaction if (page.isClosed()) { if (options.forceDebug) { console.log(`[interaction] Page is closed, skipping element interaction`); } return; } // Very short timeout since page should already be loaded await page.waitForSelector('body', { timeout: 1000 }); // Re-check after async wait — page may have closed during selector wait if (page.isClosed()) return; } catch (bodyWaitErr) { if (options.forceDebug) { console.log(`[interaction] Page not ready for element interaction: ${bodyWaitErr.message}`); } return; } // Use cached viewport for better performance const viewport = await getCachedViewport(page); const maxX = viewport.width; const maxY = viewport.height; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { // Find visible, clickable elements const elements = await page.evaluate((selectors, avoidWords, textPreviewLen) => { const clickableElements = []; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { const rect = el.getBoundingClientRect(); const isVisible = rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth; if (isVisible) { const text = (el.textContent || el.alt || el.title || '').toLowerCase(); const shouldAvoid = avoidWords && avoidWords.some(word => text.includes(word)); if (!shouldAvoid) { clickableElements.push({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height, text: text.substring(0, textPreviewLen) }); } } }); }); return clickableElements; }, elementTypes, avoidDestructive ? ['delete', 'remove', 'submit', 'buy', 'purchase', 'order'] : [], ELEMENT_INTERACTION.TEXT_PREVIEW_LENGTH); if (elements.length > 0) { // Choose a random element to interact with const element = elements[Math.floor(Math.random() * elements.length)]; // Move to element and click const currentPos = generateRandomCoordinates(maxX, maxY); await humanLikeMouseMove(page, currentPos.x, currentPos.y, element.x, element.y); // Brief pause before clicking await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN)); await page.mouse.click(element.x, element.y); // Brief pause after clicking await fastTimeout(TIMING.POST_CLICK_MIN + Math.random() * (TIMING.POST_CLICK_MAX - TIMING.POST_CLICK_MIN)); } } catch (elementErr) { // Continue to next attempt if this one fails continue; } } } catch (mainErr) { // Silently handle errors - element interaction is supplementary } } /** * Clicks random spots in the page content area to trigger document-level * onclick handlers (Monetag onclick_static, similar popunder SDKs). * * WHY THIS EXISTS: * Ad onclick SDKs attach a single listener on `document` (capture phase) * that fires on ANY click with `isTrusted: true`. They don't care which * element was clicked — just that a real input event reached the document. * `interactWithElements()` hunts for <button>/<a> which may not exist or * may be excluded by the SDK's own filter. This function simply clicks * the content area of the page where the SDK will always accept the event. * * TIMING: * - 300ms preDelay: small buffer after mouse/scroll activity (~1.2s) for * any late-loading async ad scripts to finish registering listeners. * - Spaces clicks 300-500ms apart (above Monetag's 250ms cooldown). * - Total time: ~1.1s for 2 clicks (preDelay + move + pause + click + gap). * * TARGETING: * - Clicks within the inner 60% of the viewport to avoid sticky headers, * footers, sidebars, cookie banners, and overlay close buttons. * - Each click gets a fresh random position with natural mouse approach. * * @param {import('puppeteer').Page} page * @param {object} [options] * @param {number} [options.clicks] Number of click attempts * @param {number} [options.preDelay] Ms to wait before first click * @param {number} [options.interClickMin] Min ms between clicks * @param {number} [options.interClickMax] Max ms between clicks * @param {boolean} [options.forceDebug] Log click coordinates */ async function performContentClicks(page, options = {}) { const { clicks = CONTENT_CLICK.CLICK_COUNT, preDelay = CONTENT_CLICK.PRE_CLICK_DELAY, interClickMin = CONTENT_CLICK.INTER_CLICK_MIN, interClickMax = CONTENT_CLICK.INTER_CLICK_MAX, forceDebug = false } = options; try { if (page.isClosed()) return; const viewport = await getCachedViewport(page); const inset = CONTENT_CLICK.VIEWPORT_INSET; const minX = Math.floor(viewport.width * inset); const maxX = Math.floor(viewport.width * (1 - inset)); const minY = Math.floor(viewport.height * inset); const maxY = Math.floor(viewport.height * (1 - inset)); // Wait for ad scripts to register their listeners await fastTimeout(preDelay); let lastX = minX + Math.floor(Math.random() * (maxX - minX)); let lastY = minY + Math.floor(Math.random() * (maxY - minY)); for (let i = 0; i < clicks; i++) { try { if (page.isClosed()) break; } catch { break; } // Random position in content zone const targetX = minX + Math.floor(Math.random() * (maxX - minX)); const targetY = minY + Math.floor(Math.random() * (maxY - minY)); // Natural mouse approach (few steps, no need for elaborate curves) await humanLikeMouseMove(page, lastX, lastY, targetX, targetY, { steps: CONTENT_CLICK.MOUSE_APPROACH_STEPS, curve: 0.03 + Math.random() * 0.04, jitter: 1 }); // Brief human-like pause, then click await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN)); await page.mouse.click(targetX, targetY); if (forceDebug) { console.log(`[interaction] Content click ${i + 1}/${clicks} at (${targetX}, ${targetY})`); } lastX = targetX; lastY = targetY; // Inter-click gap (skip after last click) if (i < clicks - 1) { await fastTimeout(interClickMin + Math.random() * (interClickMax - interClickMin)); } } } catch (err) { // Content clicks are supplementary — never break the scan } } /** * Simulates realistic typing behavior with human characteristics * * TYPING CHARACTERISTICS: * - Variable delay between keystrokes * - Optional typing mistakes with correction * - Realistic backspace timing * - Character-by-character typing (not paste) * * MISTAKE SIMULATION: * - Random wrong characters (2% default rate) * - Pause after mistake (human realization delay) * - Backspace to correct * - Continue with correct character * * DEVELOPER NOTES: * - Requires an active input field with focus * - All typing errors are silently handled * - Mistake rate should be low (0.01-0.05) for realism * - Use for form filling or search simulation * * @param {import('puppeteer').Page} page - Puppeteer page object * @param {string} text - Text to type * @param {object} options - Typing configuration * @param {number} options.minDelay - Minimum delay between keystrokes * @param {number} options.maxDelay - Maximum delay between keystrokes * @param {boolean} options.mistakes - Enable typing mistakes * @param {number} options.mistakeRate - Probability of mistakes (0.0-1.0) * * @example * // Basic typing * await simulateTyping(page, "hello world"); * * // Slow typing with mistakes * await simulateTyping(page, "search query", { * minDelay: 100, maxDelay: 300, mistakes: true, mistakeRate: 0.03 * }); * * // Fast typing without mistakes * await simulateTyping(page, "username", { * minDelay: 30, maxDelay: 80, mistakes: false * }); */ async function simulateTyping(page, text, options = {}) { const { minDelay = TIMING.TYPING_MIN_DELAY, maxDelay = TIMING.TYPING_MAX_DELAY, mistakes = false, mistakeRate = ELEMENT_INTERACTION.MISTAKE_RATE } = options; try { // Ensure page is ready for typing try { await page.waitForSelector('body', { timeout: 1000 }); } catch (bodyWaitErr) { return; // Silently skip typing if page not ready } for (let i = 0; i < text.length; i++) { const char = text[i]; // Simulate occasional typing mistakes if (mistakes && Math.random() < mistakeRate) { const wrongChar = String.fromCharCode(97 + Math.floor(Math.random() * 26)); await page.keyboard.type(wrongChar); await fastTimeout(TIMING.MISTAKE_PAUSE_MIN + Math.random() * (TIMING.MISTAKE_PAUSE_MAX - TIMING.MISTAKE_PAUSE_MIN)); await page.keyboard.press('Backspace'); await fastTimeout(TIMING.BACKSPACE_DELAY_MIN + Math.random() * (TIMING.BACKSPACE_DELAY_MAX - TIMING.BACKSPACE_DELAY_MIN)); } await page.keyboard.type(char); // Variable delay between keystrokes const delay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; await fastTimeout(delay); } } catch (typingErr) { // Silently handle typing errors } } /** * Cleans up interaction-related memory and cached data * Should be called periodically in long-running sessions * * @param {boolean} force - Force cleanup regardless of timing */ function cleanupInteractionMemory(force = false) { interactionMemoryCleanupCounter++; // Only cleanup every 10 calls unless forced if (!force && interactionMemoryCleanupCounter % 10 !== 0) { return; } // Clear cached viewport if it's older than cache duration const now = Date.now(); if (cachedViewport && (now - lastViewportCheck) > VIEWPORT_CACHE_DURATION) { cachedViewport = null; lastViewportCheck = 0; } } /** * Performs comprehensive page interaction simulation - MAIN ENTRY POINT * * This is the primary function called by nwss.js for page interaction. * It orchestrates multiple interaction types based on configuration. * * INTERACTION SEQUENCE: * 1. Move mouse to random starting position * 2. Perform configured number of mouse movements * 3. Add occasional pauses for realism * 4. Simulate scrolling (if enabled) * 5. Interact with elements (if enabled) * 6. End with final hover position * * INTENSITY LEVELS: * - LOW: 2 movements, 1 scroll, 50% longer pauses * - MEDIUM: 3 movements, 2 scrolls, normal timing * - HIGH: 5 movements, 3 scrolls, 30% faster timing * * SAFETY FEATURES: * - All errors are caught and logged (won't break main scan) * - Element clicking is disabled by default * - Destructive actions are avoided * - Respects viewport boundaries * * PERFORMANCE NOTES: * - Duration is distributed across all actions * - Actions are time-spaced for even distribution * - Intensity affects both quantity and timing * * @param {import('puppeteer').Page} page - Puppeteer page object * @param {string} currentUrl - Current page URL for logging * @param {object} options - Interaction configuration * @param {number} options.mouseMovements - Number of mouse movements * @param {boolean} options.includeScrolling - Enable scrolling simulation * @param {boolean} options.includeElementClicks - Enable element clicking * @param {boolean} options.includeTyping - Enable typing simulation * @param {number} options.duration - Total interaction time in milliseconds * @param {string} options.intensity - 'low' | 'medium' | 'high' * @param {boolean} forceDebug - Enable debug logging * * @example * // Basic interaction * await performPageInteraction(page, 'https://example.com'); * * // High intensity interaction * await performPageInteraction(page, 'https://news.com', { * intensity: 'high', * duration: 5000, * includeScrolling: true * }); * * // Minimal interaction * await performPageInteraction(page, 'https://shop.com', { * intensity: 'low', * mouseMovements: 1, * includeScrolling: false, * includeElementClicks: false * }); */ async function performPageInteraction(page, currentUrl, options = {}, forceDebug = false) { const { mouseMovements = INTENSITY_SETTINGS.MEDIUM.movements, includeScrolling = true, includeElementClicks = false, includeTyping = false, duration = TIMING.DEFAULT_INTERACTION_DURATION, intensity = 'medium' } = options; try { // CRITICAL: Emergency timeout wrapper for entire interaction const MAX_INTERACTION_TIME = 15000; // 15 seconds absolute maximum const interactionStartTime = Date.now(); const checkTimeout = () => { return Date.now() - interactionStartTime > MAX_INTERACTION_TIME; }; // Validate page state before starting interaction try { if (page.isClosed()) { if (forceDebug) { console.log(`[interaction] Page is closed for ${currentUrl}, skipping interaction`); } return; } } catch { return; } // Use cached viewport for better performance const viewport = await getCachedViewport(page); const maxX = viewport.width; const maxY = viewport.height; if (forceDebug) { let hostname = currentUrl; try { hostname = new URL(currentUrl).hostname; } catch {} console.log(`[interaction] Starting enhanced interaction simulation for ${hostname} (${intensity} intensity)`); } // Configure intensity settings const settings = INTENSITY_SETTINGS[intensity.toUpperCase()] || INTENSITY_SETTINGS.MEDIUM; const actualMovements = Math.min(mouseMovements, settings.movements); // Start with random position let currentPos = generateRandomCoordinates(maxX, maxY, { preferEdges: true }); // Batch mouse move operations for better performance try { await page.mouse.move(currentPos.x, currentPos.y); } catch (mouseMoveErr) { return; // Exit gracefully if mouse operations fail } const totalDuration = duration * settings.pauseMultiplier; // CRITICAL: Cap action intervals to prevent long waits const baseInterval = totalDuration / (actualMovements + (includeScrolling ? settings.scrolls : 0)); const actionInterval = Math.min(baseInterval, 100); // Never wait more than 100ms // Start timing ONLY the actual interaction operations const actualInteractionStartTime = Date.now(); // Perform mouse movements for (let i = 0; i < actualMovements; i++) { if (checkTimeout()) break; // Emergency timeout check const targetPos = generateRandomCoordinates(maxX, maxY, { avoidCenter: i % 2 === 0, preferEdges: i % 3 === 0 }); await humanLikeMouseMove(page, currentPos.x, currentPos.y, targetPos.x, targetPos.y, { steps: 3 + Math.floor(Math.random() * 4), // CRITICAL: 3-6 steps (was 8-18) curve: 0.05 + Math.random() * 0.05, // CRITICAL: Minimal curve jitter: 1 + Math.random() * 2 }); currentPos = targetPos; // Occasional pause if (Math.random() < PROBABILITIES.PAUSE_CHANCE) { await fastTimeout(25 + Math.random() * 50); // CRITICAL: Much shorter pauses } // Time-based spacing await fastTimeout(Math.min(actionInterval, 50)); // CRITICAL: Cap at 50ms } // Scrolling simulation if (includeScrolling) { for (let i = 0; i < settings.scrolls; i++) { if (checkTimeout()) break; // Emergency timeout check const direction = Math.random() < PROBABILITIES.SCROLL_DOWN_BIAS ? 'down' : 'up'; await simulateScrolling(page, { direction, amount: 1 + Math.floor(Math.random() * 2), // CRITICAL: Less scrolling smoothness: 1 + Math.floor(Math.random() * 2) // CRITICAL: Much less smooth }); await fastTimeout(Math.min(actionInterval, 100)); // CRITICAL: Cap intervals } } // Click interaction — two strategies for maximum ad script coverage // 1. Content area clicks: triggers document-level onclick handlers // (Monetag, similar popunder SDKs that listen on document) // 2. Element clicks: interacts with specific UI elements // (ad scripts that attach to specific clickable elements) if (includeElementClicks) { if (checkTimeout()) return; // Emergency timeout check // Primary: content area clicks for document-level onclick handlers await performContentClicks(page, { forceDebug }); // Secondary: targeted element clicks (fast, 1 attempt only) if (!checkTimeout()) { await interactWithElements(page, { maxAttempts: 1, avoidDestructive: true }); } } // Periodic memory cleanup during interaction cleanupInteractionMemory(); // Final hover position const finalPos = generateRandomCoordinates(maxX, maxY); await humanLikeMouseMove(page, currentPos.x, currentPos.y, finalPos.x, finalPos.y); // Safe hover with validation try { const bodyElement = await page.$('body'); if (bodyElement) { try { await page.hover('body'); } finally { await bodyElement.dispose(); } } } catch (hoverErr) { // Silently handle hover failures - not critical } // End timing ONLY after actual interaction operations complete const interactionElapsedTime = Date.now() - actualInteractionStartTime // CRITICAL: Warn about slow interactions if (interactionElapsedTime > 8000) { console.warn(`[interaction] WARNING: Interaction took ${interactionElapsedTime}ms for ${currentUrl}`); } if (forceDebug) { console.log(`[interaction] Completed interaction simulation in ${interactionElapsedTime}ms (${actualMovements} movements, ${includeScrolling ? settings.scrolls : 0} scrolls)`); } } catch (interactionErr) { if (forceDebug) { console.log(`[interaction] Interaction simulation failed for ${currentUrl}: ${interactionErr.message}`); } // Don't throw - interaction failures shouldn't break the main scan } } /** * Creates an optimized interaction configuration based on site characteristics * * This function analyzes the target URL and creates an appropriate interaction * configuration automatically. It can be overridden by explicit site config. * * AUTOMATIC SITE DETECTION: * - News/Blog sites: High intensity, longer duration, more scrolling * - Shopping sites: Low intensity, avoid clicking (safety) * - Social/Forum sites: Medium intensity, balanced interaction * - Default: Medium intensity for unknown sites * * CONFIGURATION PRIORITY: * 1. Explicit siteConfig parameters (highest priority) * 2. URL-based automatic detection * 3. Default values (lowest priority) * * SITE CONFIG OVERRIDES: * - interact_intensity: 'low' | 'medium' | 'high' * - interact_duration: milliseconds * - interact_scrolling: boolean * - interact_clicks: boolean * - interact_typing: boolean * * DEVELOPER NOTES: * - Add new site patterns by modifying the hostname checks * - Site detection is case-insensitive substring matching * - Returns a complete config object with all required properties * - Gracefully handles malformed URLs * * @param {string} url - Site URL for analysis * @param {object} siteConfig - Site-specific configuration overrides * @returns {object} Optimized interaction configuration * * @example * // Automatic configuration * const config = createInteractionConfig('https://news.example.com'); * // Returns: { intensity: 'high', duration: 3000, includeScrolling: true, ... } * * // With manual overrides * const config = createInteractionConfig('https://shop.com', { * interact_intensity: 'medium', * interact_clicks: true * }); * // Returns: { intensity: 'medium', includeElementClicks: true, ... } * * // Custom site pattern * const config = createInteractionConfig('https://custom-forum.com'); * // Falls back to default configuration */ function createInteractionConfig(url, siteConfig = {}) { try { const hostname = new URL(url).hostname.toLowerCase(); // Site-specific interaction patterns const config = { mouseMovements: 3, includeScrolling: true, includeElementClicks: false, includeTyping: false, duration: 2000, intensity: 'medium' }; // Adjust based on site type if (hostname.includes('news') || hostname.includes('blog')) { config.includeScrolling = true; config.intensity = 'high'; config.duration = Math.min(SITE_DURATIONS.NEWS_BLOG, 4000); // FIXED: Cap at 4 seconds } else if (hostname.includes('shop') || hostname.includes('store')) { config.includeElementClicks = false; // Avoid accidental purchases config.intensity = 'low'; } else if (hostname.includes('social') || hostname.includes('forum')) { config.includeScrolling = true; config.mouseMovements = 4; config.intensity = 'medium'; config.duration = SITE_DURATIONS.SOCIAL_FORUM; } // Override with explicit site configuration if (siteConfig.interact_intensity) { config.intensity = siteConfig.interact_intensity; } if (siteConfig.interact_duration) { config.duration = siteConfig.interact_duration; } if (siteConfig.interact_scrolling !== undefined) { config.includeScrolling = siteConfig.interact_scrolling; } if (siteConfig.interact_clicks !== undefined) { config.includeElementClicks = siteConfig.interact_clicks; } return config; } catch (urlErr) { // Return default config if URL parsing fails return { mouseMovements: INTENSITY_SETTINGS.MEDIUM.movements, includeScrolling: true, includeElementClicks: false, includeTyping: false, duration: TIMING.DEFAULT_INTERACTION_DURATION, intensity: 'medium' }; } } // === MODULE EXPORTS === // Export all public functions for use by nwss.js and other modules /** * MAIN EXPORTS - Primary functions for page interaction * * performPageInteraction: Main entry point for comprehensive interaction * createInteractionConfig: Auto-generates optimized config based on URL */ /** * COMPONENT EXPORTS - Individual interaction components * * humanLikeMouseMove: Realistic mouse movement with curves * simulateScrolling: Smooth scrolling simulation * interactWithElements: Safe element clicking * simulateTyping: Human-like typing with mistakes * generateRandomCoordinates: Smart coordinate generation */ /** * USAGE EXAMPLES: * * // In nwss.js (main integration) * const { performPageInteraction, createInteractionConfig } = require('./lib/interaction'); * const config = createInteractionConfig(url, siteConfig); * await performPageInteraction(page, url, config, debug); * * // Custom interaction script * const { humanLikeMouseMove, simulateScrolling } = require('./lib/interaction'); * await humanLikeMouseMove(page, 0, 0, 500, 300); * await simulateScrolling(page, { direction: 'down', amount: 3 }); * * // Advanced coordinate generation * const { generateRandomCoordinates } = require('./lib/interaction'); * const pos = generateRandomCoordinates(1920, 1080, { preferEdges: true }); */ module.exports = { // Main interaction functions performPageInteraction, createInteractionConfig, getCachedViewport, cleanupInteractionMemory, // Component functions for custom implementations humanLikeMouseMove, simulateScrolling, interactWithElements, performContentClicks, simulateTyping, generateRandomCoordinates };