const puppeteer = require('puppeteer');
const _ = require('lodash');
class Actor {
// ================ Setup ================
browser;
page;
identity;
config;
pos;
ready = false;
/**
* Accept construction variables
*
* @param config
* @param identity
*/
constructor(config, identity) {
this.identity = identity; // Login information etc
this.config = config; // Platform specific variables
}
/**
* Initialize plugins and set stat to ready
*
* @returns {Promise<void>}
*/
async init() {
return new Promise(async (resolve, reject) => {
try {
this.browser = await puppeteer.launch({
headless: false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36',
'--lang=en-US,en;q=0.9'
],
userDataDir: this.identity.cookies, // Eg: './storage/identities/username'
defaultViewport: this.identity.browser.viewport
});
this.page = await this.browser.newPage();
this.ready = true;
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Your primary browse function, can rename to whatever you like
*
* @returns {Promise<void>}
*/
async browse() {
return new Promise(async (resolve, reject) => {
try {
// This is where you would build your bot actions
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Scroll the page, optionally with a direction or distance
*
* @returns {Promise<void>}
* @param direction
* @param distance
*/
async scroll(direction, distance) {
return new Promise(async (resolve, reject) => {
try {
this.logStart('Scroll');
const noise = this.config.noise;
const selectors = this.config.selectors;
const viewport = this.identity.browser.viewport;
// Limit to viewport
let limits = {
min: Math.ceil(viewport.height * 0.30),
max: Math.ceil(viewport.height * 0.85)
};
distance = distance ? distance : _.random(limits.min, limits.max);
if (!direction || (direction !== 'up' && direction !== 'down')) {
// Roll scroll direction
direction = await this.chance(noise.scroll.probability) ? 'down' : 'up';
// Detect header or footer tag
const footer = await this.page.$(selectors.footer);
if (footer && await footer.isIntersectingViewport()) {
direction = 'up';
}
else {
const header = await this.page.$(selectors.header);
if (header && await header.isIntersectingViewport()) {
direction = 'down';
}
}
}
distance = direction === 'down' ? distance : (distance * -1);
await this.page.evaluate(async distance => {
return new Promise((resolve, reject) => {
try {
window.scrollBy(0, distance);
resolve();
} catch (error) {
reject(this.error(error));
}
});
}, distance);
await this.delay(100, 500);
resolve();
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Handles delays with noise
*
* @returns {Promise<void>}
* @param min
* @param max
*/
async delay(min, max) {
return new Promise(async (resolve, reject) => {
try {
const noise = this.config.noise;
let delay = _.random(min ? min : noise.delay.min, max ? max : noise.delay.max);
await this.page.waitFor(delay);
resolve();
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Moves the mouse to a position and adds noise
*
* @returns {Promise<void>}
* @param pos
* @param delay
*/
async moveMouseTo(pos, delay) {
return new Promise(async (resolve, reject) => {
try {
delay = delay ? delay : _.random(10, 80);
await this.page.mouse.move(pos.x + _.random(0, 2), pos.y + _.random(0, 2));
await this.delay(delay, (delay + 8));
this.pos = pos;
resolve();
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Move the mouse a set or random number of times around the page
*
* @returns {Promise<void>}
* @param count
*/
async randomMouseMovement(count) {
return new Promise(async (resolve, reject) => {
try {
const noise = this.config.noise;
await this.delay(50, 80);
count = count ? parseInt(count) : _.random(noise.mouse.sequence.min, noise.mouse.sequence.max);
for (let i = 0; i < count; i++) {
let startPos = await this.getCurrentMousePosition();
let endPos = await this.getRandomMousePosition();
await this.panMouse(startPos, endPos); // You will need to implement your own panning function
}
await this.delay(50, 1000);
resolve();
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Select a new random mouse position from within the viewport
*
* @returns {Promise<void>}
*/
async getRandomMousePosition() {
return new Promise(async (resolve, reject) => {
try {
const viewport = this.identity.browser.viewport;
let pos = {
x: Math.ceil(viewport.width * _.random(0.10, 0.90)),
y: Math.ceil(viewport.height * _.random(0.10, 0.90))
};
resolve(pos);
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Finds a dom element on the page
*
* @returns {Promise<void>}
* @param selector
*/
async getElement(selector) {
return new Promise(async (resolve, reject) => {
try {
const element = (typeof selector === 'string') ? (await this.page.$(selector)) : selector;
resolve(element);
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Gets the x,y position of a dom element
*
* @returns {Promise<void>}
* @param selector
*/
async getElementPosition(selector) {
return new Promise(async (resolve, reject) => {
try {
const element = await this.getElement(selector);
if (element) {
const boundingBox = await element.boundingBox();
let pos = {
x: Math.ceil(boundingBox.x + (boundingBox.width * _.random(0.3, 0.7))),
y: Math.ceil(boundingBox.x + (boundingBox.height * _.random(0.3, 0.7))),
};
resolve(pos);
}
else {
resolve();
}
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Returns the current mouse position or starts at a new position if none found
*
* @returns {Promise<void>}
*/
async getCurrentMousePosition() {
return new Promise(async (resolve, reject) => {
try {
let pos = this.pos;
if (!(pos && pos.x && pos.y)) {
pos = await this.getRandomMousePosition();
this.pos = pos;
}
resolve(pos);
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Helper function for handling randomness
*
* @returns {Promise<void>}
* @param probability
*/
async chance(probability) {
return new Promise(async (resolve, reject) => {
try {
probability = probability ? parseInt(probability) : 50;
resolve(!!_.random(0, 100) < probability);
} catch (error) {
reject(this.error(error));
}
});
}
/**
* Error handler that prints error
*
* @returns {Promise<void>}
* @param error
*/
error(error) {
const string = _.get(error, 'message', error);
console.error(string);
return error;
}
}
module.exports = Actor;