UNPKG

puppeteer-extra-plugin-human-typing

Version:

> A [puppeteer-extra](https://github.com/berstend/puppeteer-extra) plugin to add human typing to Puppeteer.

244 lines (188 loc) 7.48 kB
const { PuppeteerExtraPlugin } = require("puppeteer-extra-plugin"); const BACKSPACE = "Backspace"; class PuppeteerExtraPluginHumanTyping extends PuppeteerExtraPlugin { constructor(options = {}) { super(options); this.keyboardLayout = this.opts.keyboardLayouts[this.opts.keyboardLayout || "en"]; } get name() { return "human-typing"; } get defaults() { return { backspaceMaximumDelayInMs: 750 * 2, backspaceMinimumDelayInMs: 750, chanceToKeepATypoInPercent: 0, keyboardLayout: "de", keyboardLayouts: { de: [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "ß"], ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p", "ü"], ["a", "s", "d", "f", "g", "h", "j", "k", "l", "ö", "ä"], ["y", "x", "c", "v", "b", "n", "m", ",", ".", "-"], ], en: [ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-"], ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "["], ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'"], ["z", "x", "c", "v", "b", "n", "m", ",", ".", "/"], ], }, maximumDelayInMs: 650, minimumDelayInMs: 150, typoChanceInPercent: 15, }; } _addCustomMethods(page) { page.typeHuman = async (selector, text, options) => this._typeHuman(page, selector, text, (options = {})); } _delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } _getCharacterCoordinates(character) { const characterLowerCased = character.toLowerCase(); if (!this._isInKeyboardLayout(characterLowerCased)) { return null; } const keyboardLayout = this.keyboardLayout; for (let row = 0; row < keyboardLayout.length; row++) { for (let column = 0; column < keyboardLayout[row].length; column++) { if (keyboardLayout[row][column] === characterLowerCased) { return { column, row, }; } } } return null; } _getCharacterCloseTo(character, attempt = 0) { if (attempt > 3) { return character; } const characterLowerCased = character.toLowerCase(); if (!this._isInKeyboardLayout(characterLowerCased)) { return character; } const characterCoordinates = this._getCharacterCoordinates(character); if (characterCoordinates === null) { return character; } const isCharacterLowerCase = character.toLowerCase() === character; let possibleColumns = []; let possibleRows = []; const _c = characterCoordinates.column; const _r = characterCoordinates.row; /** A higher chance to select characters to the left and right, than above and below. (75%) */ if (this._getRandomIntegerBetween(0, 100) <= 75) { possibleRows = [characterCoordinates.row]; } else { if (_r === 0) { possibleRows = [_r, _r + 1]; } else if (_r === this.keyboardLayout.length - 1) { possibleRows = [_r - 1, _r]; } else { possibleRows = [_r - 1, _r, _r + 1]; } } if (_c === 0) { possibleColumns = [_c, _c + 1]; } else if (_c === this.keyboardLayout[_r].length - 1) { possibleColumns = [_c - 1, _c]; } else { possibleColumns = [_c - 1, _c, _c + 1]; } const selectedColumn = possibleColumns[Math.floor(Math.random() * possibleColumns.length)]; const selectedRow = possibleRows[Math.floor(Math.random() * possibleRows.length)]; const result = this.keyboardLayout[selectedRow][selectedColumn] ?? null; /** This can happen because each line ("row") must not have the same length/number of letters ("columns"). */ if (result === null) { return character; } /** If we accidentally get the same character, we try again (but no more than 5 times). */ if (result === characterLowerCased) { return this._getCharacterCloseTo(character, attempt + 1); } return isCharacterLowerCase ? result : result.toUpperCase(); } _getRandomIntegerBetween(a, b) { return Math.floor(Math.random() * (b - a + 1) + a); } _getTypingFlow(text) { const typingFlow = []; const characters = text.split(""); for (let i = 0; i < characters.length; i++) { const character = characters[i]; const characterLowerCased = character.toLowerCase(); /** We take one third of "typoChanceInPercent" to write a space twice. However, we will not remove this one. */ const hasSpaceTypo = character === " " && this._getRandomIntegerBetween(0, 100) <= this.opts.typoChanceInPercent / 3; if (hasSpaceTypo) { typingFlow.push(" "); } const hasTypo = this._isInKeyboardLayout(characterLowerCased) && this._getRandomIntegerBetween(0, 100) <= this.opts.typoChanceInPercent; if (hasTypo) { typingFlow.push(this._getCharacterCloseTo(character)); typingFlow.push(BACKSPACE); } typingFlow.push(character); /** We take half of "typoChanceInPercent" to write a character twice. */ const hasDoubleCharacterTypo = this._isInKeyboardLayout(characterLowerCased) && this._getRandomIntegerBetween(0, 100) <= this.opts.typoChanceInPercent / 2; if (hasDoubleCharacterTypo) { typingFlow.push(character); typingFlow.push(BACKSPACE); } } return typingFlow; } _isInKeyboardLayout(character) { for (const row of this.keyboardLayout) { if (row.includes(character)) { return true; } } return false; } async _typeHuman(page, selector, text, options = {}) { await page.focus(selector); const typingFlow = this._getTypingFlow(text); const backspaceMaximumDelayInMs = options.backspaceMaximumDelayInMs || this.opts.backspaceMinimumDelayInMs; const backspaceMinimumDelayInMs = options.backspaceMinimumDelayInMs || this.opts.backspaceMinimumDelayInMs; const maximumDelayInMs = options.maximumDelayInMs || this.opts.maximumDelayInMs; const minimumDelayInMs = options.minimumDelayInMs || this.opts.minimumDelayInMs; for (const character of typingFlow) { if (character === BACKSPACE) { if (this._getRandomIntegerBetween(0, 100) > this.options.chanceToKeepATypoInPercent) { continue; } await this._delay(this._getRandomIntegerBetween(backspaceMinimumDelayInMs, backspaceMaximumDelayInMs)); await page.keyboard.press(character, { delay: this._getRandomIntegerBetween(50, 300), }); } else { await this._delay(this._getRandomIntegerBetween(minimumDelayInMs, maximumDelayInMs)); await page.keyboard.press(character, { delay: this._getRandomIntegerBetween(50, 300), }); } } } async onBrowser(browser) { const pages = await browser.pages(); for (const page of pages) { this._addCustomMethods(page); for (const frame of page.mainFrame().childFrames()) { this._addCustomMethods(frame); } } } async onPageCreated(page) { this._addCustomMethods(page); } } const defaultExport = (options) => { return new PuppeteerExtraPluginHumanTyping(options || {}); }; module.exports = defaultExport;