- import axios, playwright-firefox, otplib, and node modules for enhanced functionality - add utility imports from local modules for better code organization - define URL_CLAIM, COOKIES_PATH, and BEARER_TOKEN_NAME constants for clearer code structure
163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
import axios from 'axios';
|
|
import { firefox } from 'playwright-firefox';
|
|
import { authenticator } from 'otplib';
|
|
import path from 'node:path';
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import {
|
|
resolve,
|
|
jsonDb,
|
|
datetime,
|
|
stealth,
|
|
filenamify,
|
|
prompt,
|
|
notify,
|
|
html_game_list,
|
|
handleSIGINT
|
|
} from './src/util.js';
|
|
import { cfg } from './src/config.js';
|
|
|
|
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
|
|
const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json');
|
|
const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN';
|
|
|
|
const ensureLoggedIn = async (page, context) => {
|
|
const isLoggedIn = async () => {
|
|
try {
|
|
return await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
|
} catch (err) {
|
|
console.error('Error checking login status:', err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const attemptAutoLogin = async () => {
|
|
// Epic login form
|
|
if (!cfg.eg_email || !cfg.eg_password) return false;
|
|
try {
|
|
await page.goto('https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: cfg.login_timeout
|
|
});
|
|
|
|
// Add more robust selector handling
|
|
const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first();
|
|
const passwordField = page.locator('input[name="password"], input#password').first();
|
|
const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first();
|
|
|
|
// Debugging logging
|
|
console.log('Login page loaded, checking email field');
|
|
|
|
// step 1: email + continue
|
|
if (await emailField.count() > 0) {
|
|
await emailField.fill(cfg.eg_email);
|
|
await continueBtn.click().catch(err => {
|
|
console.error('Error clicking continue button:', err);
|
|
});
|
|
}
|
|
|
|
// step 2: password + submit
|
|
await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
|
|
await passwordField.fill(cfg.eg_password);
|
|
|
|
const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
|
|
if (await rememberMe.count() > 0) await rememberMe.check().catch(() => { });
|
|
|
|
await continueBtn.click().catch(async (err) => {
|
|
console.error('Error clicking continue button:', err);
|
|
await page.click('button[type="submit"]').catch(() => {});
|
|
});
|
|
|
|
// MFA step
|
|
try {
|
|
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
|
const otp = cfg.eg_otpkey
|
|
? authenticator.generate(cfg.eg_otpkey)
|
|
: await prompt({
|
|
type: 'text',
|
|
message: 'Enter two-factor sign in code',
|
|
validate: n => n.toString().length == 6 || 'The code must be 6 digits!'
|
|
});
|
|
|
|
const codeInputs = page.locator('input[name^="code-input"]');
|
|
if (await codeInputs.count() > 0) {
|
|
const digits = otp.toString().split('');
|
|
for (let i = 0; i < digits.length; i++) {
|
|
const input = codeInputs.nth(i);
|
|
await input.fill(digits[i]);
|
|
}
|
|
} else {
|
|
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
|
}
|
|
|
|
await continueBtn.click().catch(async () => {
|
|
await page.click('button[type="submit"]').catch(() => {});
|
|
});
|
|
} catch (mfaError) {
|
|
console.warn('MFA step failed or not needed:', mfaError);
|
|
}
|
|
|
|
await page.waitForURL('**/free-games', { timeout: cfg.login_timeout }).catch(err => {
|
|
console.error('Failed to navigate to free games page:', err);
|
|
});
|
|
|
|
return await isLoggedIn();
|
|
} catch (err) {
|
|
console.error('Auto login failed:', err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const isChallenge = async () => {
|
|
const cfFrame = page.locator('iframe[title*="Cloudflare"], iframe[src*="challenges"]');
|
|
const cfText = page.locator('text=Verify you are human');
|
|
return await cfFrame.count() > 0 || await cfText.count() > 0;
|
|
};
|
|
|
|
let loginAttempts = 0;
|
|
const MAX_LOGIN_ATTEMPTS = 3;
|
|
|
|
while (!await isLoggedIn() && loginAttempts < MAX_LOGIN_ATTEMPTS) {
|
|
loginAttempts++;
|
|
console.error(`Not signed in (Attempt ${loginAttempts}). Trying automatic login, otherwise please login in the browser.`);
|
|
|
|
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
|
|
|
|
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout);
|
|
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
|
|
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
|
|
|
if (await isChallenge()) {
|
|
console.warn('Cloudflare challenge detected. Solve the captcha in the browser (no automation).');
|
|
await notify('epic-games (new): Cloudflare challenge, please solve manually in browser.');
|
|
await page.waitForTimeout(cfg.login_timeout);
|
|
continue;
|
|
}
|
|
|
|
const logged = await attemptAutoLogin();
|
|
if (logged) break;
|
|
|
|
console.log('Waiting for manual login in the browser (cookies might be invalid).');
|
|
await notify('epic-games (new): please login in browser; cookies invalid or expired.');
|
|
|
|
if (cfg.headless) {
|
|
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
|
await context.close();
|
|
process.exit(1);
|
|
}
|
|
|
|
await page.waitForTimeout(cfg.login_timeout);
|
|
}
|
|
|
|
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
|
|
console.error('Maximum login attempts reached. Exiting.');
|
|
await context.close();
|
|
process.exit(1);
|
|
}
|
|
|
|
const user = await page.locator('egs-navigation').getAttribute('displayname');
|
|
console.log(`Signed in as ${user}`);
|
|
|
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
return user;
|
|
};
|