/* eslint-disable class-methods-use-this */ import 'dotenv/config'; import RandExp from 'randexp'; import { Logger } from 'pino'; import { Page, Protocol, ElementHandle } from 'puppeteer'; // import { writeFileSync } from 'fs-extra'; import { getCookiesRaw, setPuppeteerCookies } from '../../src/common/request'; import { EPIC_CLIENT_ID, STORE_HOMEPAGE_EN } from '../../src/common/constants'; import { getHcaptchaCookies } from '../../src/puppet/hcaptcha'; import puppeteer, { toughCookieFileStoreToPuppeteerCookie, getDevtoolsUrl, launchArgs, } from '../../src/common/puppeteer'; import logger from '../../src/common/logger'; import Smtp4Dev from './smtp4dev'; import { LocalNotifier } from '../../src/notifiers'; const NOTIFICATION_TIMEOUT = 24 * 60 * 60 * 1000; export interface AccountManagerProps { username?: string; password?: string; totp?: string; country?: string; } export default class AccountManager { private smtp4dev: Smtp4Dev; public email: string; public username: string; public password: string; public country: string; public totp?: string; private addressHost = process.env.CREATION_EMAIL_HOST || ''; private L: Logger; constructor(props?: AccountManagerProps) { if (props?.username) { this.username = props?.username; } else { const randUser = new RandExp(/[0-9a-zA-Z]{8,16}/); this.username = randUser.gen(); } if (props?.password) { this.password = props?.password; } else { const randPass = new RandExp(/[a-zA-Z]{4,8}[0-9]{3,8}/); this.password = randPass.gen(); } this.country = props?.country || 'United States'; this.totp = props?.totp; this.smtp4dev = new Smtp4Dev({ apiBaseUrl: process.env.SMTP4DEV_URL || '', requestExtensions: { username: process.env.SMTP4DEV_USER, password: process.env.SMTP4DEV_PASSWORD, }, }); this.email = `${this.username}@${this.addressHost}`; this.L = logger.child({ username: this.username, }); } public logAccountDetails(): void { this.L.info({ email: this.email, password: this.password, totp: this.totp, }); } public async createAccount(): Promise<void> { this.L.info({ username: this.username, password: this.password, email: this.email }); const hCaptchaCookies = await getHcaptchaCookies(); const userCookies = await getCookiesRaw(this.email); const puppeteerCookies = toughCookieFileStoreToPuppeteerCookie(userCookies); this.L.debug('Logging in with puppeteer'); const browser = await puppeteer.launch(launchArgs); const page = await browser.newPage(); this.L.trace(getDevtoolsUrl(page)); const cdpClient = await page.target().createCDPSession(); await cdpClient.send('Network.setCookies', { cookies: [...puppeteerCookies, ...hCaptchaCookies], }); await page.setCookie(...puppeteerCookies, ...hCaptchaCookies); await page.goto( `https://www.epicgames.com/id/register/epic?redirect_uri=${STORE_HOMEPAGE_EN}&client_id=${EPIC_CLIENT_ID}`, { waitUntil: 'networkidle0' } ); await this.fillDOB(page); await this.fillSignUpForm(page); await this.handleCaptcha(page); await this.fillEmailVerificationForm(page); this.L.trace('Saving new cookies'); const currentUrlCookies = (await cdpClient.send('Network.getAllCookies')) as { cookies: Protocol.Network.Cookie[]; }; await browser.close(); setPuppeteerCookies(this.email, currentUrlCookies.cookies); this.L.info({ username: this.username, password: this.password, email: this.email }); } private async fillDOB(page: Page): Promise<void> { this.L.trace('Getting date fields'); const [monthInput, dayInput, yearInput] = await Promise.all([ page.waitForSelector(`#month`) as Promise<ElementHandle<HTMLDivElement>>, page.waitForSelector(`#day`) as Promise<ElementHandle<HTMLDivElement>>, page.waitForSelector(`#year`) as Promise<ElementHandle<HTMLInputElement>>, ]); await monthInput.click(); const month1 = (await page.waitForSelector( `ul.MuiList-root > li` )) as ElementHandle<HTMLLIElement>; await month1.click(); await page.waitForTimeout(500); // idk why this is required await dayInput.click(); const day1 = (await page.waitForSelector( `ul.MuiList-root > li` )) as ElementHandle<HTMLLIElement>; await day1.click(); await yearInput.type(this.getRandomInt(1970, 2002).toString()); const continueButton = (await page.waitForSelector( `#continue:not([disabled])` )) as ElementHandle<HTMLButtonElement>; await page.waitForTimeout(500); // idk why this is required this.L.trace('Clicking continueButton'); await continueButton.click({ delay: 100 }); } public async deleteAccount(): Promise<void> { this.L.info({ email: this.email }, 'Deleting account'); const hCaptchaCookies = await getHcaptchaCookies(); const userCookies = await getCookiesRaw(this.email); const puppeteerCookies = toughCookieFileStoreToPuppeteerCookie(userCookies); this.L.debug('Logging in with puppeteer'); const browser = await puppeteer.launch(launchArgs); const page = await browser.newPage(); this.L.trace(getDevtoolsUrl(page)); const cdpClient = await page.target().createCDPSession(); await cdpClient.send('Network.setCookies', { cookies: [...puppeteerCookies, ...hCaptchaCookies], }); await page.setCookie(...puppeteerCookies, ...hCaptchaCookies); await page.goto(`https://www.epicgames.com/account/personal`, { waitUntil: 'networkidle0' }); this.L.trace('Waiting for deleteButton'); const deleteButton = (await page.waitForXPath( `//button[contains(., 'Request Account Delete')]` )) as ElementHandle<HTMLButtonElement>; this.L.trace('Clicking deleteButton'); await deleteButton.click({ delay: 100 }); this.L.trace('Waiting for securityCodeInput'); const securityCodeInput = (await page.waitForSelector( `input[name='security-code']` )) as ElementHandle<HTMLInputElement>; const code = await this.getActionVerification(); this.L.trace('Filling securityCodeInput'); await securityCodeInput.type(code); this.L.trace('Waiting for confirmButton'); const confirmButton = (await page.waitForXPath( `//button[contains(., 'Confirm Delete Request')]` )) as ElementHandle<HTMLButtonElement>; this.L.trace('Clicking confirmButton'); await confirmButton.click({ delay: 100 }); this.L.trace('Waiting for skipSurveyButton'); const skipSurveyButton = (await page.waitForSelector( `button#deletion-reason-skip` )) as ElementHandle<HTMLButtonElement>; this.L.trace('Clicking skipSurveyButton'); await skipSurveyButton.click({ delay: 100 }); await page.waitForSelector(`div.account-deletion-request-success-modal`); this.L.debug('Account deletion successful'); this.L.trace('Saving new cookies'); const currentUrlCookies = (await cdpClient.send('Network.getAllCookies')) as { cookies: Protocol.Network.Cookie[]; }; await browser.close(); setPuppeteerCookies(this.email, currentUrlCookies.cookies); } private async fillSignUpForm(page: Page): Promise<void> { this.L.trace('Getting sign up fields'); const randName = new RandExp(/[a-zA-Z]{3,12}/); const [ countryInput, firstNameInput, lastNameInput, displayNameInput, emailInput, passwordInput, tosInput, ] = await Promise.all([ page.waitForSelector(`#country`) as Promise<ElementHandle<Element>>, page.waitForSelector(`#name`) as Promise<ElementHandle<HTMLInputElement>>, page.waitForSelector(`#lastName`) as Promise<ElementHandle<HTMLInputElement>>, page.waitForSelector(`#displayName`) as Promise<ElementHandle<HTMLInputElement>>, page.waitForSelector(`#email`) as Promise<ElementHandle<HTMLInputElement>>, page.waitForSelector(`#password`) as Promise<ElementHandle<HTMLInputElement>>, page.waitForSelector(`#tos`) as Promise<ElementHandle<HTMLInputElement>>, ]); await countryInput.type(this.country); await firstNameInput.type(randName.gen()); await lastNameInput.type(randName.gen()); await displayNameInput.type(this.username); await emailInput.type(this.email); await passwordInput.type(this.password); await tosInput.click(); const submitButton = (await page.waitForSelector( `#btn-submit:not([disabled])` )) as ElementHandle<HTMLButtonElement>; this.L.trace('Clicking submitButton'); await submitButton.click({ delay: 100 }); } private async waitForHCaptcha(page: Page): Promise<'captcha' | 'nav'> { try { const talonHandle = await page.$('iframe#talon_frame_registration_prod'); if (!talonHandle) throw new Error('Could not find talon_frame_registration_prod'); const talonFrame = await talonHandle.contentFrame(); if (!talonFrame) throw new Error('Could not find talonFrame contentFrame'); this.L.trace('Waiting for hcaptcha iframe'); await talonFrame.waitForSelector(`#challenge_container_hcaptcha > iframe[src*="hcaptcha"]`, { visible: true, }); return 'captcha'; } catch (err) { if (err.message.includes('timeout')) { throw err; } if (err.message.includes('detached')) { this.L.trace(err); } else { this.L.warn(err); } return 'nav'; } } private async handleCaptcha(page: Page): Promise<void> { const action = await this.waitForHCaptcha(page); if (action === 'nav') return; this.L.debug('Captcha detected'); const url = await page.openPortal(); this.L.info({ url }, 'Go to this URL and do something'); // eslint-disable-next-line @typescript-eslint/no-explicit-any new LocalNotifier(null as any).sendNotification(url); await page.waitForSelector(`input[name='code-input-0']`, { timeout: NOTIFICATION_TIMEOUT, }); await page.closePortal(); } private async fillEmailVerificationForm(page: Page): Promise<void> { this.L.trace('Working on email verification form'); const code = await this.getVerification(); this.L.trace('Waiting for codeInput'); const codeInput = (await page.waitForSelector( `input[name='code-input-0']` )) as ElementHandle<HTMLInputElement>; await codeInput.click({ delay: 100 }); await page.keyboard.type(code); this.L.trace('Waiting for continueButton'); const continueButton = (await page.waitForSelector( `#continue:not([disabled])` )) as ElementHandle<HTMLButtonElement>; this.L.trace('Clicking continueButton'); await continueButton.click(); await page.waitForNavigation({ waitUntil: 'networkidle2' }); } private async getVerification(): Promise<string> { this.L.debug('Waiting for creation verification email'); const message = await this.smtp4dev.findNewEmailTo(this.username); const emailSource = await this.smtp4dev.getMessageSource(message.id); // writeFileSync('email-source.eml', emailSource, 'utf8'); const codeRegexp = /\\t\\t([0-9]{6})\\n/g; const matches = codeRegexp.exec(emailSource); if (!matches) throw new Error('No code matches'); const code = matches[1].trim(); this.L.debug({ code }, 'Email code'); return code; } private async getActionVerification(): Promise<string> { this.L.debug('Waiting for action verification email'); const message = await this.smtp4dev.findNewEmailTo(this.username); const emailSource = await this.smtp4dev.getMessageSource(message.id); // writeFileSync('email-source.eml', emailSource, 'utf8'); const codeRegexp = / +([0-9]{6})<br\/><br\/>/g; const matches = codeRegexp.exec(emailSource); if (!matches) throw new Error('No code matches'); const code = matches[1].trim(); this.L.debug({ code }, 'Email code'); return code; } private getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1) + min); } }