From b783f7c1fa43fe088728184df0592fd8c5c2b057 Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sat, 6 Dec 2025 15:02:08 +0100 Subject: [PATCH] feat: implement HumanTyping module to enhance typing simulation and avoid bot detection --- src/account-creation/AccountCreator.ts | 10 +- src/functions/Login.ts | 9 +- src/functions/activities/Search.ts | 10 +- src/functions/activities/SearchOnBing.ts | 6 +- src/functions/login/TotpHandler.ts | 9 +- src/util/browser/HumanTyping.ts | 170 +++++++++++++++++++++++ 6 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 src/util/browser/HumanTyping.ts diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index 9629400..63a5c3a 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -2633,17 +2633,17 @@ export class AccountCreator { log(false, 'CREATOR', `Normal click failed: ${error1}`, 'log', 'yellow') } - // Strategy 2: Force click (if cookie banner blocks) + // Strategy 2: JavaScript click (avoid force: true - bot detection risk) if (!clickSuccess) { try { - log(false, 'CREATOR', '🔄 Retrying with force click...', 'log', 'cyan') - await getStartedButton.click({ force: true, timeout: 5000 }) + log(false, 'CREATOR', '🔄 Retrying with JavaScript click...', 'log', 'cyan') + await getStartedButton.evaluate((el: HTMLElement) => el.click()) await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_GET_STARTED_RETRY', 5000) clickSuccess = true - log(false, 'CREATOR', '✅ Clicked "Get started" with force', 'log', 'green') + log(false, 'CREATOR', '✅ Clicked "Get started" with JS', 'log', 'green') } catch (error2) { - log(false, 'CREATOR', `Force click failed: ${error2}`, 'log', 'yellow') + log(false, 'CREATOR', `JS click failed: ${error2}`, 'log', 'yellow') } } diff --git a/src/functions/Login.ts b/src/functions/Login.ts index e19fb92..877770a 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -4,6 +4,7 @@ import type { Page } from 'playwright' import { MicrosoftRewardsBot } from '../index' import { OAuth } from '../interface/OAuth' +import { HumanTyping } from '../util/browser/HumanTyping' import { waitForElementSmart, waitForPageReady } from '../util/browser/SmartWait' import { Retry } from '../util/core/Retry' import { logError } from '../util/notifications/Logger' @@ -622,8 +623,8 @@ export class Login { }) if (!prefilledResult.found) { - await page.fill(SELECTORS.emailInput, '') - await page.fill(SELECTORS.emailInput, email) + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection + await HumanTyping.typeEmail(page.locator(SELECTORS.emailInput), email) } else { this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled') } @@ -684,8 +685,8 @@ export class Login { const blocked = await this.securityDetector.detectSignInBlocked(page) if (blocked) return - await page.fill(SELECTORS.passwordInput, '') - await page.fill(SELECTORS.passwordInput, password) + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection + await HumanTyping.typePassword(page.locator(SELECTORS.passwordInput), password) const submitResult = await waitForElementSmart(page, SELECTORS.submitBtn, { initialTimeoutMs: 500, diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts index 33f9d42..af18c62 100644 --- a/src/functions/activities/Search.ts +++ b/src/functions/activities/Search.ts @@ -6,6 +6,7 @@ import { Workers } from '../Workers' import { AxiosRequestConfig } from 'axios' import { Counters, DashboardData } from '../../interface/DashboardData' import { GoogleSearch } from '../../interface/Search' +import { HumanTyping } from '../../util/browser/HumanTyping' import { waitForElementSmart } from '../../util/browser/SmartWait' type GoogleTrendsResponse = [ @@ -232,15 +233,18 @@ export class Search extends Workers { let navigatedDirectly = false try { - // Try focusing and filling instead of clicking (more reliable on mobile) + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection await box.focus({ timeout: 2000 }).catch(() => { /* ignore focus errors */ }) - await box.fill('') await this.bot.utils.wait(200) + + // Clear field using keyboard (natural) await searchPage.keyboard.down(platformControlKey) await searchPage.keyboard.press('A') await searchPage.keyboard.press('Backspace') await searchPage.keyboard.up(platformControlKey) - await box.type(query, { delay: 20 }) + + // FIXED: Use HumanTyping for natural search query entry + await HumanTyping.type(box, query, 1.5) // Fast typing (familiar search action) await searchPage.keyboard.press('Enter') } catch (typeErr) { // As a robust fallback, navigate directly to the search results URL diff --git a/src/functions/activities/SearchOnBing.ts b/src/functions/activities/SearchOnBing.ts index c512a48..61f77a0 100644 --- a/src/functions/activities/SearchOnBing.ts +++ b/src/functions/activities/SearchOnBing.ts @@ -3,6 +3,7 @@ import path from 'path' import type { Page } from 'playwright' import { DELAYS } from '../../constants' +import { HumanTyping } from '../../util/browser/HumanTyping' import { Workers } from '../Workers' import { MorePromotion, PromotionalItem } from '../../interface/DashboardData' @@ -27,9 +28,10 @@ export class SearchOnBing extends Workers { await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS) try { await box.focus({ timeout: DELAYS.THIS_OR_THAT_START }).catch(() => { /* ignore */ }) - await box.fill('') await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS) - await page.keyboard.type(query, { delay: DELAYS.TYPING_DELAY }) + + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection + await HumanTyping.type(box, query, 1.5) // Fast typing (familiar search action) await page.keyboard.press('Enter') } catch { const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}` diff --git a/src/functions/login/TotpHandler.ts b/src/functions/login/TotpHandler.ts index a179e42..57acd9c 100644 --- a/src/functions/login/TotpHandler.ts +++ b/src/functions/login/TotpHandler.ts @@ -1,6 +1,7 @@ import type { Locator, Page } from 'playwright' import readline from 'readline' import { MicrosoftRewardsBot } from '../../index' +import { HumanTyping } from '../../util/browser/HumanTyping' import { logError } from '../../util/notifications/Logger' import { generateTOTP } from '../../util/security/Totp' @@ -190,8 +191,8 @@ export class TotpHandler { return } - // Fill code and submit - await page.fill('input[name="otc"]', code) + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection + await HumanTyping.typeTotp(page.locator('input[name="otc"]'), code) await page.keyboard.press('Enter') this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code submitted') } catch (error) { @@ -248,8 +249,8 @@ export class TotpHandler { this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn') return } - await input.fill('') - await input.fill(code) + // FIXED: Use HumanTyping instead of .fill() to avoid bot detection + await HumanTyping.typeTotp(input, code) // Use unified selector system const submit = await this.findFirstVisibleLocator(page, TotpHandler.TOTP_SELECTORS.submit) if (submit) { diff --git a/src/util/browser/HumanTyping.ts b/src/util/browser/HumanTyping.ts new file mode 100644 index 0000000..fbdf84e --- /dev/null +++ b/src/util/browser/HumanTyping.ts @@ -0,0 +1,170 @@ +/** + * Human-Like Typing Module for Login & Bot Operations + * + * CRITICAL: Microsoft detects .fill() as instant bot signature + * This module provides gradual character-by-character typing with natural variance + * + * DIFFERENCES from account-creation/HumanBehavior: + * - Login typing is FASTER (humans type passwords quickly from muscle memory) + * - No typo simulation (users rarely make typos in saved credentials) + * - Shorter delays (login is familiar action, not form-filling) + * + * IMPORTANT: Keep separate from account-creation to avoid coupling + */ + +import type { Locator } from 'rebrowser-playwright' + +export class HumanTyping { + /** + * Type text naturally into field (FAST login typing) + * + * CRITICAL: Use this instead of .fill() for ALL text inputs + * + * @param locator Playwright locator (input field) + * @param text Text to type + * @param speed Typing speed multiplier (1.0 = normal, 0.5 = slow, 2.0 = fast) + * @returns Promise + * + * @example + * await HumanTyping.type(page.locator('#email'), 'user@example.com', 1.2) // Fast typing + */ + static async type(locator: Locator, text: string, speed: number = 1.0): Promise { + // SECURITY: Ensure field is visible before typing (avoid bot detection) + try { + await locator.waitFor({ state: 'visible', timeout: 5000 }) + } catch { + // Field not visible - continue anyway (page may be slow) + } + + // IMPROVEMENT: Focus field naturally (humans click before typing) + await locator.focus().catch(() => { + // Focus failed - not critical + }) + + // CRITICAL: Clear existing text first (simulate Ctrl+A + Delete) + await locator.clear().catch(async () => { + // .clear() failed - use keyboard fallback + await locator.press('Control+a').catch(() => { }) + await locator.press('Backspace').catch(() => { }) + }) + + // IMPROVEMENT: Short pause after clearing (human reaction time) + await this.delay(50, 150) + + // Type each character with variable timing + for (let i = 0; i < text.length; i++) { + const char = text[i] + if (!char) continue // Skip undefined characters + + // SECURITY: Natural typing speed variance (login = fast, familiar action) + // Base speed: 40-80ms per character (fast typing) + // Speed multiplier: adjustable per context + const baseDelay = 40 + Math.random() * 40 // 40-80ms + const charDelay = Math.floor(baseDelay / speed) + + // IMPROVEMENT: Slower on special characters (humans need to find keys) + const isSpecialChar = /[^a-zA-Z0-9@.]/.test(char) + const finalDelay = isSpecialChar ? charDelay * 1.5 : charDelay + + await locator.pressSequentially(char, { delay: finalDelay }).catch(() => { + // Typing failed - continue (character may have been typed) + }) + + // IMPROVEMENT: Occasional micro-pauses (10% chance) + if (Math.random() < 0.1 && i > 0) { + await this.delay(100, 300) + } + + // IMPROVEMENT: Burst typing pattern (humans type groups of characters quickly) + // 30% chance to type next 2-3 characters rapidly + if (Math.random() < 0.3 && i < text.length - 2) { + const burstLength = Math.floor(Math.random() * 2) + 2 // 2-3 chars + for (let j = 0; j < burstLength && i + 1 < text.length; j++) { + i++ + const nextChar = text[i] + if (nextChar) { + await locator.pressSequentially(nextChar, { delay: 10 }).catch(() => { }) + } + } + } + } + + // IMPROVEMENT: Short pause after typing (human verification) + await this.delay(100, 300) + } + + /** + * Type email address (optimized for email format) + * + * PATTERN: Humans type emails in 3 parts: [name] @ [domain] + * + * @param locator Playwright locator (email input) + * @param email Email address + * @returns Promise + */ + static async typeEmail(locator: Locator, email: string): Promise { + const [localPart, domain] = email.split('@') + + if (!localPart || !domain) { + // Invalid email format - fallback to regular typing + await this.type(locator, email, 1.2) + return + } + + // IMPROVEMENT: Type local part (fast) + await this.type(locator, localPart, 1.3) + + // IMPROVEMENT: Slight pause before @ (humans verify username) + await this.delay(50, 200) + + // Type @ symbol (slightly slower - special key) + await locator.pressSequentially('@', { delay: 100 }).catch(() => { }) + + // IMPROVEMENT: Slight pause after @ (humans verify domain) + await this.delay(50, 150) + + // Type domain (fast) + await this.type(locator, domain, 1.4) + } + + /** + * Type password (FAST - humans type passwords from muscle memory) + * + * PATTERN: Password typing is FASTEST (no reading, pure muscle memory) + * + * @param locator Playwright locator (password input) + * @param password Password string + * @returns Promise + */ + static async typePassword(locator: Locator, password: string): Promise { + // CRITICAL: Passwords typed 2x faster than regular text + await this.type(locator, password, 2.0) + } + + /** + * Type TOTP code (6-digit code from authenticator) + * + * PATTERN: TOTP typed VERY FAST (user reading from phone, focus) + * + * @param locator Playwright locator (TOTP input) + * @param code 6-digit TOTP code + * @returns Promise + */ + static async typeTotp(locator: Locator, code: string): Promise { + // CRITICAL: TOTP codes typed EXTREMELY fast (user focused, limited time) + // Speed: 3.0x faster (15-25ms per character) + await this.type(locator, code, 3.0) + } + + /** + * Human-like delay with natural variance + * + * @param minMs Minimum delay (ms) + * @param maxMs Maximum delay (ms) + * @returns Promise + */ + private static async delay(minMs: number, maxMs: number): Promise { + const delay = Math.floor(Math.random() * (maxMs - minMs) + minMs) + await new Promise(resolve => setTimeout(resolve, delay)) + } +}