Skip to content

How do I use multiple profiles? #149

@tripoloski-it

Description

@tripoloski-it

I have a class creating a "client" profile:

import { createHash } from 'crypto';
import { existsSync } from 'fs';
import { join } from 'path';
import puppeteer, { Browser, Page } from 'puppeteer-core';
import { createPlugin, PuppeteerFingerprintPlugin } from 'puppeteer-with-fingerprints';
import { env } from '../config';
import {
	AUTHORIZE_SELECTORS,
	AuthorizeStatus,
	BROWSER_WORKING_DIRECTORY,
	DEFAULT_ACCOUNT_BEFORE_LOAD,
	DEFAULT_CLIENT_STATE,
	PROFILES_PATH,
	WHITELIST_URLS_WHEN_CAPTCHA_ACTIVE,
} from '../constants';
import { Logger } from '../services';
import { IAccountInfo, IClientOptions, IClientRequest, IClientState } from '../types';
import { ClientResponse } from './client-response.core';

export class Client {
	public static async create(accountData: IClientOptions) {
		const plugin = await this.initPlugin(accountData.email, accountData.proxy);
		if (!plugin) return null;

		const browserAndPage = await this.initBrowserAndPage(plugin, accountData.email);
		if (!browserAndPage) return null;

		const { browser, page } = browserAndPage;

		const client = new Client(plugin, browser, page, accountData);
		await client.setupPage();

		return client;
	}
	private static async initBrowserAndPage(plugin: PuppeteerFingerprintPlugin, email: string) {
		let browser: Browser | null = null;
		try {
			browser = await plugin.launch({
				userDataDir: this.getProfilePath(email),
				args: ['--no-sandbox'],
				headless: false,
			});

			const page = await browser.newPage();

			return { browser, page };
		} catch (error) {
			Logger.fatal(email, error, 'BrowserAndPage');
			await browser?.close();
		}
	}
	private static async initPlugin(email: string, proxy: string) {
		try {
			const plugin = createPlugin(puppeteer);
			const profilePath = this.getProfilePath(email);
			const hasProfile = existsSync(profilePath);

			plugin.setServiceKey(env.fingerprintSwitcherToken);
			plugin.setWorkingFolder(BROWSER_WORKING_DIRECTORY);

			if (!hasProfile) {
				const fingerprint = await plugin.fetch({
					tags: ['Desktop', 'Microsoft Windows', 'Chrome'],
					timeLimit: '60 days',
				});
				plugin.useFingerprint(fingerprint);
				plugin.useProxy(proxy);
				return plugin;
			}

			plugin.useProfile(this.getProfilePath(email), {
				loadFingerprint: true,
				loadProxy: true,
			});

			return plugin;
		} catch (error) {
			Logger.fatal(email, error, 'InitPlugin');
			return null;
		}
	}
	private static getProfilePath(email: string) {
		const hash = createHash('md5').update(email, 'utf-8').digest('hex');
		return join(PROFILES_PATH, hash);
	}

	private accountInfo: IAccountInfo = DEFAULT_ACCOUNT_BEFORE_LOAD;
	private state: IClientState = DEFAULT_CLIENT_STATE;
	private logger: Logger;

	private constructor(
		private plugin: PuppeteerFingerprintPlugin,
		private browser: Browser,
		private page: Page,
		private options: IClientOptions,
	) {
		this.logger = Logger.get(options.email);
	}

	public async authorize() {
		// ===
	}
	public async close() {
		this.state = {
			inAuthorize: false,
			isLoggedIn: false,
			isBlockedByCaptcha: false,
			isBrowserOpen: false,
			isClientClosed: true,
		};
		await this.browser.close();
		this.logger.info('Browser unloaded');
	}
	public async request(options: IClientRequest) {
		if (this.browser.connected || this.page.isClosed() || !this.state.isLoggedIn) return null;

		const data = await this.page.evaluate(async request => {
			try {
				const response = await fetch(request.url, {
					credentials: 'include',
					method: request.method,
					body: request.body,
					headers: request.headers,
				});
				const headers = Object.fromEntries(response.headers.entries());
				const arrayBuffer = await response.arrayBuffer();

				return {
					request,
					response: {
						isOk: response.ok,
						status: response.status,
						statusText: response.statusText,
						headers,
						arrayBuffer,
					},
				};
			} catch (error) {
				return null;
			}
		}, options);
		if (!data) return null;

		return new ClientResponse(data.request, data.response);
	}

	public updateProxy(validatedProxy: string) {
		this.options.proxy = validatedProxy;
		this.plugin.useProxy(this.options.proxy);
		this.logger.info(`Client: Proxy updated (Need restart browser)`);
	}
	public getState(): Readonly<IClientState> {
		return this.state;
	}
	public getPlugin() {
		return this.plugin;
	}
	public getBrowser() {
		return this.browser;
	}
	public getPage() {
		return this.page;
	}
	public getAccountInfo(): Readonly<IAccountInfo> {
		return this.accountInfo;
	}

	public get profileName() {
		return createHash('md5').update(this.options.email, 'utf-8').digest('hex');
	}

	private async setupPage() {
		await this.page.setRequestInterception(true);
		await this.page.emulateTimezone('Asia/Yakutsk');

		this.page.on('request', request => {
			const url = request.url();
			if (this.state.isClientClosed) return request.abort('aborted');
			if (this.state.isBlockedByCaptcha) {
				if (!this.isRequestAllowedWhenCaptchaActive(url)) {
					this.logger.info(`Requests: Request to ${url} blocked (Captcha whitelist)`);
					return request.abort('blockedbyclient');
				}
			}
		});
		this.page.on('response', async response => {
			const url = response.url();
			if (url.includes('captcha')) {
				this.state.isBlockedByCaptcha = true;
				const captchaImage = await response.buffer();
				this.logger.info('Responses: Need captcha solving');
			}
		});
	}
	private isRequestAllowedWhenCaptchaActive(url: string) {
		return WHITELIST_URLS_WHEN_CAPTCHA_ACTIVE.some(chunk => url.includes(chunk));
	}
}

Am I doing the right thing by creating a new plugin for each profile? But at the same time, they all have one working engine folder.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions