feat: Introduire un simulateur de comportement humain pour améliorer la création de comptes

This commit is contained in:
Obsidian-wtf
2025-11-15 12:00:35 +01:00
parent 22cbaa166a
commit 41d06ee001
2 changed files with 296 additions and 36 deletions

View File

@@ -4,10 +4,12 @@ import * as readline from 'readline'
import type { BrowserContext, Page } from 'rebrowser-playwright'
import { log } from '../util/notifications/Logger'
import { DataGenerator } from './DataGenerator'
import { HumanBehavior } from './HumanBehavior'
import { CreatedAccount } from './types'
export class AccountCreator {
private page!: Page
private human!: HumanBehavior
private dataGenerator: DataGenerator
private referralUrl?: string
private recoveryEmail?: string
@@ -27,10 +29,10 @@ export class AccountCreator {
this.rlClosed = false
}
// Human-like delay helper
// Human-like delay helper (DEPRECATED - use this.human.humanDelay() instead)
// Kept for backward compatibility during migration
private async humanDelay(minMs: number, maxMs: number): Promise<void> {
const delay = Math.random() * (maxMs - minMs) + minMs
await this.page.waitForTimeout(Math.floor(delay))
await this.human.humanDelay(minMs, maxMs)
}
/**
@@ -618,14 +620,25 @@ export class AccountCreator {
try {
this.page = await context.newPage()
log(false, 'CREATOR', '🚀 Starting account creation...', 'log', 'cyan')
// CRITICAL: Initialize human behavior simulator
this.human = new HumanBehavior(this.page)
log(false, 'CREATOR', '🚀 Starting account creation with enhanced anti-detection...', 'log', 'cyan')
// Navigate to signup page
await this.navigateToSignup()
// CRITICAL: Simulate human reading the signup page
await this.human.microGestures('SIGNUP_PAGE')
await this.humanDelay(500, 1500)
// Click "Create account" button
await this.clickCreateAccount()
// CRITICAL: Simulate human inspecting the email field
await this.human.microGestures('EMAIL_FIELD')
await this.humanDelay(300, 800)
// Generate email and fill it (handles suggestions automatically)
const emailResult = await this.generateAndFillEmail(this.autoAccept)
if (!emailResult) {
@@ -635,6 +648,10 @@ export class AccountCreator {
log(false, 'CREATOR', `✅ Email: ${emailResult}`, 'log', 'green')
// CRITICAL: Simulate human reading password requirements
await this.human.microGestures('PASSWORD_PAGE')
await this.humanDelay(500, 1200)
// Wait for password page and fill it
const password = await this.fillPassword()
if (!password) {
@@ -653,6 +670,10 @@ export class AccountCreator {
const finalEmail = await this.extractEmail()
const confirmedEmail = finalEmail || emailResult
// CRITICAL: Simulate human inspecting birthdate fields
await this.human.microGestures('BIRTHDATE_PAGE')
await this.humanDelay(400, 1000)
// Fill birthdate
const birthdate = await this.fillBirthdate()
if (!birthdate) {
@@ -667,6 +688,10 @@ export class AccountCreator {
return null
}
// CRITICAL: Simulate human inspecting name fields
await this.human.microGestures('NAMES_PAGE')
await this.humanDelay(400, 1000)
// Fill name fields
const names = await this.fillNames(confirmedEmail)
if (!names) {
@@ -942,10 +967,8 @@ export class AccountCreator {
// Microsoft separates username from domain for outlook.com/hotmail.com addresses
const emailFillSuccess = await this.retryOperation(
async () => {
await emailInput.clear()
await this.humanDelay(800, 1500)
await emailInput.fill(email)
await this.humanDelay(1200, 2500)
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(emailInput, email, 'EMAIL_INPUT')
// SMART VERIFICATION: Check if Microsoft separated the domain
const inputValue = await emailInput.inputValue().catch(() => '')
@@ -1080,10 +1103,8 @@ export class AccountCreator {
// CRITICAL: Retry fill with SMART verification (handles domain separation)
const retryFillSuccess = await this.retryOperation(
async () => {
await emailInput.clear()
await this.humanDelay(800, 1500)
await emailInput.fill(newEmail)
await this.humanDelay(1200, 2500)
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(emailInput, newEmail, 'EMAIL_RETRY')
// SMART VERIFICATION: Microsoft may separate domain for managed email providers
const inputValue = await emailInput.inputValue().catch(() => '')
@@ -1158,10 +1179,8 @@ export class AccountCreator {
const retryFillSuccess = await this.retryOperation(
async () => {
await emailInput.clear()
await this.humanDelay(800, 1500)
await emailInput.fill(newEmail)
await this.humanDelay(1200, 2500)
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(emailInput, newEmail, 'EMAIL_AUTO_RETRY')
// SMART VERIFICATION: Microsoft may separate domain for managed email providers
const inputValue = await emailInput.inputValue().catch(() => '')
@@ -1380,10 +1399,8 @@ export class AccountCreator {
// CRITICAL: Retry fill with verification
const passwordFillSuccess = await this.retryOperation(
async () => {
await passwordInput.clear()
await this.humanDelay(800, 1500) // INCREASED from 500-1000
await passwordInput.fill(password)
await this.humanDelay(1200, 2500) // INCREASED from 800-2000
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(passwordInput, password, 'PASSWORD_INPUT')
// Verify value was filled correctly
const verified = await this.verifyInputValue('input[type="password"]', password)
@@ -1461,7 +1478,8 @@ export class AccountCreator {
// CRITICAL: Retry click if it fails
const dayClickSuccess = await this.retryOperation(
async () => {
await dayButton.click({ force: true })
// CRITICAL FIX: Use normal click (no force) to avoid bot detection
await dayButton.click({ timeout: 5000 })
await this.humanDelay(1500, 2500) // INCREASED delay
// Verify dropdown opened
@@ -1506,7 +1524,8 @@ export class AccountCreator {
// CRITICAL: Retry click if it fails
const monthClickSuccess = await this.retryOperation(
async () => {
await monthButton.click({ force: true })
// CRITICAL FIX: Use normal click (no force) to avoid bot detection
await monthButton.click({ timeout: 5000 })
await this.humanDelay(1500, 2500) // INCREASED delay
// Verify dropdown opened
@@ -1560,10 +1579,8 @@ export class AccountCreator {
// CRITICAL: Retry fill with verification
const yearFillSuccess = await this.retryOperation(
async () => {
await yearInput.clear()
await this.humanDelay(500, 1000)
await yearInput.fill(birthdate.year.toString())
await this.humanDelay(1000, 2000)
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(yearInput, birthdate.year.toString(), 'YEAR_INPUT')
// Verify value was filled correctly
const verified = await this.verifyInputValue(
@@ -1667,10 +1684,8 @@ export class AccountCreator {
// CRITICAL: Retry fill with verification
const firstNameFillSuccess = await this.retryOperation(
async () => {
await firstNameInput.clear()
await this.humanDelay(800, 1500) // INCREASED from 500-1000
await firstNameInput.fill(names.firstName)
await this.humanDelay(1200, 2500) // INCREASED from 800-2000
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(firstNameInput, names.firstName, 'FIRSTNAME_INPUT')
return true
},
@@ -1712,10 +1727,8 @@ export class AccountCreator {
// CRITICAL: Retry fill with verification
const lastNameFillSuccess = await this.retryOperation(
async () => {
await lastNameInput.clear()
await this.humanDelay(800, 1500) // INCREASED from 500-1000
await lastNameInput.fill(names.lastName)
await this.humanDelay(1200, 2500) // INCREASED from 800-2000
// CRITICAL FIX: Use humanType() instead of .fill() to avoid detection
await this.human.humanType(lastNameInput, names.lastName, 'LASTNAME_INPUT')
return true
},
@@ -2701,8 +2714,7 @@ ${JSON.stringify(accountData, null, 2)}`
// Fill email input
const emailInput = this.page.locator('#EmailAddress').first()
await emailInput.fill(recoveryEmailToUse)
await this.humanDelay(500, 1000)
await this.human.humanType(emailInput, recoveryEmailToUse, 'RECOVERY_EMAIL')
// Click Next
const nextButton = this.page.locator('#iNext').first()

View File

@@ -0,0 +1,248 @@
/**
* Human Behavior Simulator for Account Creation
*
* CRITICAL: Microsoft detects bots by analyzing:
* 1. Typing speed (instant .fill() = bot, gradual .type() = human)
* 2. Mouse movements (no movement = bot, random moves = human)
* 3. Pauses (fixed delays = bot, variable pauses = human)
* 4. Click patterns (force clicks = bot, natural clicks = human)
*
* This module ensures account creation is INDISTINGUISHABLE from manual creation.
*/
import type { Page } from 'rebrowser-playwright'
import { log } from '../util/notifications/Logger'
export class HumanBehavior {
private page: Page
constructor(page: Page) {
this.page = page
}
/**
* Human-like delay with natural variance
* Unlike fixed delays, humans vary greatly in timing
*
* @param minMs Minimum delay
* @param maxMs Maximum delay
* @param context Description for logging (optional)
*/
async humanDelay(minMs: number, maxMs: number, context?: string): Promise<void> {
// IMPROVEMENT: Add occasional "thinking" pauses (10% chance of 2x delay)
const shouldThink = Math.random() < 0.1
const multiplier = shouldThink ? 2 : 1
const delay = (Math.random() * (maxMs - minMs) + minMs) * multiplier
if (shouldThink && context) {
log(false, 'CREATOR', `[${context}] 🤔 Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan')
}
await this.page.waitForTimeout(Math.floor(delay))
}
/**
* CRITICAL: Type text naturally like a human
* NEVER use .fill() - it's instant and detectable
*
* @param locator Playwright locator (input field)
* @param text Text to type
* @param context Description for logging
*/
async humanType(locator: import('rebrowser-playwright').Locator, text: string, context: string): Promise<void> {
// CRITICAL: Clear field first (human would select all + delete)
await locator.clear()
await this.humanDelay(300, 800, context)
// CRITICAL: Type character by character with VARIABLE delays
// Real humans type at 40-80 WPM = ~150-300ms per character
// But with natural variation: some characters faster, some slower
log(false, 'CREATOR', `[${context}] ⌨️ Typing: "${text.substring(0, 20)}${text.length > 20 ? '...' : ''}"`, 'log', 'cyan')
for (let i = 0; i < text.length; i++) {
const char: string = text[i] as string
// CRITICAL: Skip if char is somehow undefined (defensive programming)
if (!char) continue
// NATURAL VARIANCE:
// - Fast keys: common letters (e, a, t, i, o, n) = 80-150ms
// - Slow keys: numbers, symbols, shift combos = 200-400ms
// - Occasional typos: 5% chance of longer pause (user correcting)
let charDelay: number
const isFastKey = /[eatino]/i.test(char)
const isSlowKey = /[^a-z]/i.test(char) // Numbers, symbols, etc.
const hasTyro = Math.random() < 0.05 // 5% typo simulation
if (hasTyro) {
charDelay = Math.random() * 400 + 300 // 300-700ms (correcting typo)
} else if (isFastKey) {
charDelay = Math.random() * 70 + 80 // 80-150ms
} else if (isSlowKey) {
charDelay = Math.random() * 200 + 200 // 200-400ms
} else {
charDelay = Math.random() * 100 + 120 // 120-220ms
}
await locator.type(char, { delay: 0 }) // Type instantly
await this.page.waitForTimeout(Math.floor(charDelay))
}
log(false, 'CREATOR', `[${context}] ✅ Typing completed`, 'log', 'green')
// IMPROVEMENT: Random pause after typing (human reviewing input)
await this.humanDelay(500, 1500, context)
}
/**
* CRITICAL: Simulate micro mouse movements and scrolls
* Real humans constantly move mouse and scroll while reading/thinking
*
* @param context Description for logging
*/
async microGestures(context: string): Promise<void> {
try {
// 60% chance of mouse movement (humans move mouse A LOT)
if (Math.random() < 0.6) {
const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px
const y = Math.floor(Math.random() * 150) + 30 // Random y: 30-180px
const steps = Math.floor(Math.random() * 5) + 3 // 3-8 steps (smooth movement)
await this.page.mouse.move(x, y, { steps }).catch(() => {
// Mouse move failed - page may be closed or unavailable
})
// VERBOSE logging disabled - too noisy
// log(false, 'CREATOR', `[${context}] 🖱️ Mouse moved to (${x}, ${y})`, 'log', 'gray')
}
// 30% chance of scroll (humans scroll to read content)
if (Math.random() < 0.3) {
const direction = Math.random() < 0.7 ? 1 : -1 // 70% down, 30% up
const distance = Math.floor(Math.random() * 200) + 50 // 50-250px
const dy = direction * distance
await this.page.mouse.wheel(0, dy).catch(() => {
// Scroll failed - page may be closed or unavailable
})
// VERBOSE logging disabled - too noisy
// log(false, 'CREATOR', `[${context}] 📜 Scrolled ${direction > 0 ? 'down' : 'up'} ${distance}px`, 'log', 'gray')
}
} catch {
// Gesture execution failed - not critical for operation
}
}
/**
* CRITICAL: Natural click with human behavior
* NEVER use { force: true } - it bypasses visibility checks (bot pattern)
*
* @param locator Playwright locator (button/link)
* @param context Description for logging
* @param maxRetries Max click attempts (default: 3)
* @returns true if click succeeded, false otherwise
*/
async humanClick(
locator: import('rebrowser-playwright').Locator,
context: string,
maxRetries: number = 3
): Promise<boolean> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// CRITICAL: Move mouse to element first (real humans do this)
const box = await locator.boundingBox().catch(() => null)
if (box) {
// Click at random position within element (not always center)
const offsetX = Math.random() * box.width * 0.6 + box.width * 0.2 // 20-80% of width
const offsetY = Math.random() * box.height * 0.6 + box.height * 0.2 // 20-80% of height
await this.page.mouse.move(
box.x + offsetX,
box.y + offsetY,
{ steps: Math.floor(Math.random() * 3) + 2 } // 2-5 steps
).catch(() => { })
await this.humanDelay(100, 300, context) // Pause before clicking
}
// NATURAL CLICK: No force (respects visibility/interactability)
await locator.click({ force: false, timeout: 5000 })
log(false, 'CREATOR', `[${context}] ✅ Clicked successfully`, 'log', 'green')
await this.humanDelay(300, 800, context) // Pause after clicking
return true
} catch (error) {
if (attempt < maxRetries) {
log(false, 'CREATOR', `[${context}] ⚠️ Click failed (attempt ${attempt}/${maxRetries}), retrying...`, 'warn', 'yellow')
await this.humanDelay(1000, 2000, context)
} else {
const msg = error instanceof Error ? error.message : String(error)
log(false, 'CREATOR', `[${context}] ❌ Click failed after ${maxRetries} attempts: ${msg}`, 'error')
return false
}
}
}
return false
}
/**
* CRITICAL: Simulate human "reading" the page
* Real humans pause to read content before interacting
*
* @param context Description for logging
*/
async readPage(context: string): Promise<void> {
log(false, 'CREATOR', `[${context}] 👀 Reading page...`, 'log', 'cyan')
// Random scroll movements (humans scroll while reading)
const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls
for (let i = 0; i < scrollCount; i++) {
await this.microGestures(context)
await this.humanDelay(800, 2000, context)
}
// Final reading pause
await this.humanDelay(1500, 3500, context)
}
/**
* CRITICAL: Simulate dropdown interaction (more complex than simple clicks)
* Real humans: move mouse → hover → click → wait → select option
*
* @param buttonLocator Dropdown button locator
* @param optionLocator Option to select locator
* @param context Description for logging
* @returns true if interaction succeeded, false otherwise
*/
async humanDropdownSelect(
buttonLocator: import('rebrowser-playwright').Locator,
optionLocator: import('rebrowser-playwright').Locator,
context: string
): Promise<boolean> {
// STEP 1: Click dropdown button (with human behavior)
const openSuccess = await this.humanClick(buttonLocator, `${context}_OPEN`)
if (!openSuccess) return false
// STEP 2: Wait for dropdown to open (visual feedback)
await this.humanDelay(500, 1200, context)
// STEP 3: Move mouse randomly inside dropdown (human reading options)
await this.microGestures(context)
await this.humanDelay(300, 800, context)
// STEP 4: Click selected option (with human behavior)
const selectSuccess = await this.humanClick(optionLocator, `${context}_SELECT`)
if (!selectSuccess) return false
// STEP 5: Wait for dropdown to close
await this.humanDelay(500, 1200, context)
return true
}
}