rss-feedmonitor/scripts/human-behavior.js

474 lines
14 KiB
JavaScript

/**
* Human-like behavior utilities for Playwright to avoid bot detection
* Includes realistic mouse movements, scrolling, and timing variations
*/
/**
* Generate a random number between min and max (inclusive)
*/
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Generate a random float between min and max
*/
function randomFloat(min, max) {
return Math.random() * (max - min) + min;
}
/**
* Sleep for a random duration within a range
*/
export async function randomDelay(minMs = 100, maxMs = 500) {
const delay = randomInt(minMs, maxMs);
await new Promise(resolve => setTimeout(resolve, delay));
}
/**
* Generate bezier curve points for smooth mouse movement
* Uses cubic bezier with random control points for natural curves
*/
function generateBezierPath(start, end, steps = 25) {
const points = [];
// Add some randomness to control points
const cp1x = start.x + (end.x - start.x) * randomFloat(0.25, 0.4);
const cp1y = start.y + (end.y - start.y) * randomFloat(-0.2, 0.2);
const cp2x = start.x + (end.x - start.x) * randomFloat(0.6, 0.75);
const cp2y = start.y + (end.y - start.y) * randomFloat(-0.2, 0.2);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const t2 = t * t;
const t3 = t2 * t;
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const x = mt3 * start.x +
3 * mt2 * t * cp1x +
3 * mt * t2 * cp2x +
t3 * end.x;
const y = mt3 * start.y +
3 * mt2 * t * cp1y +
3 * mt * t2 * cp2y +
t3 * end.y;
points.push({ x: Math.round(x), y: Math.round(y) });
}
return points;
}
/**
* Move mouse in a realistic, smooth path with occasional overshooting
* @param {Page} page - Playwright page object
* @param {Object} target - Target coordinates {x, y}
* @param {Object} options - Movement options
*/
export async function humanMouseMove(page, target, options = {}) {
const {
overshootChance = 0.15, // 15% chance to overshoot
overshootDistance = 20, // pixels to overshoot by
steps = 25, // number of steps in the path
stepDelay = 10 // ms between steps
} = options;
// Get current mouse position (or start from a random position)
const viewport = page.viewportSize();
const start = {
x: randomInt(viewport.width * 0.3, viewport.width * 0.7),
y: randomInt(viewport.height * 0.3, viewport.height * 0.7)
};
// Decide if we should overshoot
const shouldOvershoot = Math.random() < overshootChance;
let finalTarget = target;
if (shouldOvershoot) {
// Calculate overshoot position (slightly past the target)
const angle = Math.atan2(target.y - start.y, target.x - start.x);
const overshoot = {
x: target.x + Math.cos(angle) * randomInt(5, overshootDistance),
y: target.y + Math.sin(angle) * randomInt(5, overshootDistance)
};
// Move to overshoot position first
const overshootPath = generateBezierPath(start, overshoot, steps);
for (const point of overshootPath) {
await page.mouse.move(point.x, point.y);
await new Promise(resolve => setTimeout(resolve, stepDelay));
}
// Then correct back to target
const correctionPath = generateBezierPath(overshoot, target, Math.floor(steps * 0.3));
for (const point of correctionPath) {
await page.mouse.move(point.x, point.y);
await new Promise(resolve => setTimeout(resolve, stepDelay));
}
} else {
// Normal smooth movement
const path = generateBezierPath(start, target, steps);
for (const point of path) {
await page.mouse.move(point.x, point.y);
await new Promise(resolve => setTimeout(resolve, stepDelay));
}
}
// Add a tiny random pause after reaching target
await randomDelay(50, 150);
}
/**
* Perform random mouse movements to simulate human reading/scanning
*/
export async function randomMouseMovements(page, count = 3) {
const viewport = page.viewportSize();
for (let i = 0; i < count; i++) {
const target = {
x: randomInt(100, viewport.width - 100),
y: randomInt(100, viewport.height - 100)
};
await humanMouseMove(page, target, {
overshootChance: 0.1,
steps: randomInt(15, 30)
});
await randomDelay(200, 800);
}
}
/**
* Scroll page in a human-like manner with random intervals and amounts
* @param {Page} page - Playwright page object
* @param {Object} options - Scrolling options
*/
export async function humanScroll(page, options = {}) {
const {
direction = 'down', // 'down' or 'up'
scrollCount = 3, // number of scroll actions
minScroll = 100, // minimum pixels per scroll
maxScroll = 400, // maximum pixels per scroll
minDelay = 500, // minimum delay between scrolls
maxDelay = 2000, // maximum delay between scrolls
randomDirection = false // occasionally scroll in opposite direction
} = options;
for (let i = 0; i < scrollCount; i++) {
// Determine scroll direction
let scrollDir = direction;
if (randomDirection && Math.random() < 0.15) {
scrollDir = direction === 'down' ? 'up' : 'down';
}
// Random scroll amount
const scrollAmount = randomInt(minScroll, maxScroll);
const scrollValue = scrollDir === 'down' ? scrollAmount : -scrollAmount;
// Perform scroll in small increments for smoothness
const increments = randomInt(5, 12);
const incrementValue = scrollValue / increments;
for (let j = 0; j < increments; j++) {
await page.evaluate((delta) => {
window.scrollBy(0, delta);
}, incrementValue);
await new Promise(resolve => setTimeout(resolve, randomInt(20, 50)));
}
// Random pause between scrolls (simulating reading)
await randomDelay(minDelay, maxDelay);
}
}
/**
* Scroll to a specific element in a human-like way
*/
export async function scrollToElement(page, selector, options = {}) {
const element = await page.locator(selector).first();
// Get element position
const box = await element.boundingBox();
if (!box) {
console.warn(`Element ${selector} not found or not visible`);
return;
}
// Get current scroll position
const currentScroll = await page.evaluate(() => window.scrollY);
const viewportHeight = page.viewportSize().height;
// Calculate target scroll position (element near middle of viewport)
const targetScroll = box.y + currentScroll - (viewportHeight / 2);
const scrollDistance = targetScroll - currentScroll;
// Scroll in chunks
const chunks = Math.max(3, Math.abs(Math.floor(scrollDistance / 200)));
const chunkSize = scrollDistance / chunks;
for (let i = 0; i < chunks; i++) {
await page.evaluate((delta) => {
window.scrollBy(0, delta);
}, chunkSize);
await randomDelay(50, 150);
}
await randomDelay(300, 700);
}
/**
* Click an element with human-like behavior
*/
export async function humanClick(page, selector, options = {}) {
const {
moveToElement = true,
doubleClickChance = 0.02 // 2% chance of accidental double-click
} = options;
const element = await page.locator(selector).first();
const box = await element.boundingBox();
if (!box) {
throw new Error(`Element ${selector} not found or not visible`);
}
// Calculate click position (slightly random within element bounds)
const target = {
x: box.x + randomInt(box.width * 0.3, box.width * 0.7),
y: box.y + randomInt(box.height * 0.3, box.height * 0.7)
};
if (moveToElement) {
await humanMouseMove(page, target);
}
// Random pre-click pause
await randomDelay(100, 300);
// Click
await page.mouse.click(target.x, target.y);
// Occasional accidental double-click
if (Math.random() < doubleClickChance) {
await randomDelay(50, 150);
await page.mouse.click(target.x, target.y);
}
await randomDelay(200, 500);
}
/**
* Type text with human-like timing variations
*/
export async function humanType(page, selector, text, options = {}) {
const {
minDelay = 50,
maxDelay = 150,
mistakes = 0.02 // 2% chance of typo
} = options;
await page.click(selector);
await randomDelay(200, 400);
const chars = text.split('');
let typedText = '';
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
// Occasional typo
if (Math.random() < mistakes && i < chars.length - 1) {
// Type wrong char
const wrongChar = String.fromCharCode(char.charCodeAt(0) + randomInt(-2, 2));
await page.keyboard.type(wrongChar);
await randomDelay(minDelay, maxDelay);
// Pause (realize mistake)
await randomDelay(200, 500);
// Backspace
await page.keyboard.press('Backspace');
await randomDelay(100, 200);
}
// Type correct char
await page.keyboard.type(char);
// Variable delay based on character type
let delay;
if (char === ' ') {
delay = randomInt(maxDelay * 1.5, maxDelay * 2);
} else if (char.match(/[.!?,]/)) {
delay = randomInt(maxDelay * 1.2, maxDelay * 2);
} else {
delay = randomInt(minDelay, maxDelay);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
await randomDelay(300, 600);
}
/**
* Wait for page load with random human-like observation time
*/
export async function humanWaitForLoad(page, options = {}) {
const {
minWait = 1000,
maxWait = 3000
} = options;
// Wait for network to be idle
await page.waitForLoadState('networkidle', { timeout: 30000 });
// Additional random observation time (simulating reading/scanning)
await randomDelay(minWait, maxWait);
}
/**
* Simulate reading behavior - random scrolls and mouse movements
*/
export async function simulateReading(page, duration = 5000) {
const endTime = Date.now() + duration;
while (Date.now() < endTime) {
const action = Math.random();
if (action < 0.4) {
// Scroll a bit
await humanScroll(page, {
scrollCount: 1,
minScroll: 50,
maxScroll: 200,
minDelay: 800,
maxDelay: 1500
});
} else if (action < 0.7) {
// Move mouse randomly
await randomMouseMovements(page, 1);
} else {
// Just wait (reading)
await randomDelay(1000, 2000);
}
}
}
/**
* Configure browser context with realistic human-like settings
*/
export async function getHumanizedContext(browser, options = {}) {
const {
locale = 'en-CA',
timezone = 'America/Toronto',
viewport = null
} = options;
// Random but realistic viewport sizes
const viewports = [
{ width: 1920, height: 1080 },
{ width: 1366, height: 768 },
{ width: 1536, height: 864 },
{ width: 1440, height: 900 },
{ width: 2560, height: 1440 }
];
const selectedViewport = viewport || viewports[randomInt(0, viewports.length - 1)];
// Realistic user agents (updated to current versions)
const userAgents = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
];
const context = await browser.newContext({
viewport: selectedViewport,
userAgent: userAgents[randomInt(0, userAgents.length - 1)],
locale,
timezoneId: timezone,
permissions: [],
geolocation: { latitude: 43.6532, longitude: -79.3832 }, // Toronto
colorScheme: 'light', // Always light for consistency
deviceScaleFactor: 1, // Standard scaling
hasTouch: false,
isMobile: false,
javaScriptEnabled: true,
// Add realistic headers
extraHTTPHeaders: {
'Accept-Language': 'en-CA,en-US;q=0.9,en;q=0.8',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
'Upgrade-Insecure-Requests': '1'
}
});
// Inject additional fingerprint randomization and anti-detection
await context.addInitScript(() => {
// Remove webdriver property
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// Override permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
// Add chrome property
window.chrome = {
runtime: {}
};
// Override plugins
Object.defineProperty(navigator, 'plugins', {
get: () => [
{
0: { type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
description: 'Portable Document Format',
filename: 'internal-pdf-viewer',
length: 1,
name: 'Chrome PDF Plugin'
},
{
0: { type: 'application/pdf', suffixes: 'pdf', description: '' },
description: '',
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
length: 1,
name: 'Chrome PDF Viewer'
}
]
});
});
return context;
}
export default {
randomDelay,
humanMouseMove,
randomMouseMovements,
humanScroll,
scrollToElement,
humanClick,
humanType,
humanWaitForLoad,
simulateReading,
getHumanizedContext
};