diff --git a/epic-claimer-new.js b/epic-claimer-new.js index 8026819..04b8d33 100644 --- a/epic-claimer-new.js +++ b/epic-claimer-new.js @@ -1,4 +1,3 @@ -import axios from 'axios'; import { firefox } from 'playwright-firefox'; import { authenticator } from 'otplib'; import path from 'node:path'; @@ -14,22 +13,18 @@ import { handleSIGINT, } from './src/util.js'; import { cfg } from './src/config.js'; -import { getDeviceAuths, setAccountAuth } from './src/device-auths.js'; +import { EPIC_CLIENT_ID, GRAPHQL_ENDPOINT, FREE_GAMES_PROMOTIONS_ENDPOINT, STORE_HOMEPAGE_EN, EPIC_PURCHASE_ENDPOINT, ID_LOGIN_ENDPOINT } from './src/constants.js'; +import { setPuppeteerCookies } from './src/cookie.js'; +import { getAccountAuth, setAccountAuth } from './src/device-auths.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'; - -// Screenshot Helper Function - - -// Fetch Free Games from API -const fetchFreeGamesAPI = async () => { - const resp = await axios.get('https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions', { - params: { locale: 'en-US', country: 'US', allowCountries: 'US,DE,AT,CH,GB' }, +// Fetch Free Games from API using page.evaluate (browser context) +const fetchFreeGamesAPI = async page => { + const response = await page.evaluate(async () => { + const resp = await fetch(FREE_GAMES_PROMOTIONS_ENDPOINT + '?locale=en-US&country=US&allowCountries=US,DE,AT,CH,GB'); + return await resp.json(); }); - return resp.data?.Catalog?.searchStore?.elements + + return response?.Catalog?.searchStore?.elements ?.filter(g => g.promotions?.promotionalOffers?.[0]) ?.map(g => { const offer = g.promotions.promotionalOffers[0].promotionalOffers[0]; @@ -43,147 +38,64 @@ const fetchFreeGamesAPI = async () => { }) || []; }; -// Poll for OAuth tokens -const pollForTokens = async (deviceCode, maxAttempts = 30) => { - for (let i = 0; i < maxAttempts; i++) { - try { - const params = new URLSearchParams(); - params.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'); - params.append('device_code', deviceCode); +const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games'; +const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM; +const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json'); +const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN'; - const response = await axios.post('https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token', params.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: 'Basic OThmN2U0MmMyZTNhNGY4NmE3NGViNDNmYmI0MWVkMzk6MGEyNDQ5YTItMDEwYS00NTFlLWFmZWMtM2U4MTI5MDFjNGQ3', - }, - }); - if (response.data?.access_token) { - console.log('✅ OAuth successful'); - return response.data; - } - } catch (error) { - if (error.response?.data?.error === 'authorization_pending') { - await new Promise(resolve => setTimeout(resolve, 5000)); - continue; - } - throw error; - } +// Claim game function +const claimGame = async (page, game) => { + const purchaseUrl = `https://store.epicgames.com/${game.pageSlug}`; + console.log(`🎮 ${game.title} → ${purchaseUrl}`); + const notify_game = { title: game.title, url: purchaseUrl, status: 'failed' }; + + await page.goto(purchaseUrl, { waitUntil: 'networkidle' }); + + const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"]').first(); + await purchaseBtn.waitFor({ timeout: cfg.timeout }); + const btnText = (await purchaseBtn.textContent() || '').toLowerCase(); + + if (btnText.includes('library') || btnText.includes('owned')) { + notify_game.status = 'existed'; + return notify_game; + } + if (cfg.dryrun) { + notify_game.status = 'skipped'; + return notify_game; } - throw new Error('OAuth timeout'); -}; -// Exchange token for cookies -const exchangeTokenForCookies = async accessToken => { - const response = await axios.get('https://store.epicgames.com/', { - headers: { - Authorization: `Bearer ${accessToken}`, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - }, - }); - const cookies = response.headers['set-cookie']?.map(cookie => { - const [name, value] = cookie.split(';')[0].split('='); - return { name, value, domain: '.epicgames.com', path: '/' }; - }) || []; - cookies.push({ name: BEARER_TOKEN_NAME, value: accessToken, domain: '.epicgames.com', path: '/' }); - return cookies; -}; + await purchaseBtn.click({ delay: 50 }); -// Get client credentials token (first step of OAuth flow) -const getClientCredentialsToken = async () => { try { - const response = await axios.post('https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token', { - grant_type: 'client_credentials', - }, { - auth: { - username: '98f7e42c2e3a4f86a74eb43fbb41ed39', - password: '0a2449a2-001a-451e-afec-3e812901c4d7', - }, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - return response.data.access_token; - } catch (error) { - console.error('Failed to get client credentials token:', error.response?.status || error.message); - throw error; - } -}; + await page.waitForSelector('#webPurchaseContainer iframe', { timeout: 15000 }); + const iframe = page.frameLocator('#webPurchaseContainer iframe'); -// Get device authorization code (second step of OAuth flow) -const getDeviceAuthorizationCode = async clientCredentialsToken => { - try { - const params = new URLSearchParams(); - params.append('prompt', 'login'); - - const response = await axios.post('https://account-public-service-prod.ol.epicgames.com/account/api/oauth/deviceAuthorization', params.toString(), { - headers: { - Authorization: `Bearer ${clientCredentialsToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - console.log('Device authorization response:', response.data); - // Return the correct field names (device_code vs deviceCode) - return { - deviceCode: response.data.device_code, - userCode: response.data.user_code, - verificationUriComplete: response.data.verification_uri_complete, - }; - } catch (error) { - console.error('Failed to get device authorization code:', error.response?.status || error.message); - console.error('Error response data:', error.response?.data); - throw error; - } -}; - -// Get valid authentication -const getValidAuth = async ({ otpKey, reuseCookies, cookiesPath }) => { - if (reuseCookies && existsSync(cookiesPath)) { - const cookies = JSON.parse(readFileSync(cookiesPath, 'utf8')); - const bearerCookie = cookies.find(c => c.name === BEARER_TOKEN_NAME); - if (bearerCookie?.value) { - console.log('🔄 Reusing existing bearer token from cookies'); - return { bearerToken: bearerCookie.value, cookies }; + if (cfg.eg_parentalpin) { + try { + await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 }); + await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin); + await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); + } catch { + // no PIN needed + } } - } - console.log('🔐 Starting fresh OAuth device flow (manual approval required)...'); - - // Step 1: Get client credentials token - const clientCredentialsToken = await getClientCredentialsToken(); - console.log('✅ Got client credentials token'); - - // Step 2: Get device authorization code - const { deviceCode, userCode, verificationUriComplete } = await getDeviceAuthorizationCode(clientCredentialsToken); - console.log(`📱 Open: ${verificationUriComplete}`); - console.log(`💳 Code: ${userCode}`); - - const tokens = await pollForTokens(deviceCode); - - if (otpKey) { - const totpCode = authenticator.generate(otpKey); - console.log(`🔑 TOTP Code (generated): ${totpCode}`); + await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); try { - const params = new URLSearchParams(); - params.append('grant_type', 'refresh_token'); - params.append('refresh_token', tokens.refresh_token); - params.append('client_id', '98f7e42c2e3a4f86a74eb43fbb41ed39'); - params.append('client_secret', '0a2449a2-001a-451e-afec-3e812901c4d7'); - - const refreshed = await axios.post('https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token', params.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - tokens.access_token = refreshed.data.access_token; + await iframe.locator('button:has-text("I Accept")').click({ timeout: 5000 }); } catch { - // Ignore if refresh fails; use original token + // not required } + await page.locator('text=Thanks for your order!').waitFor({ state: 'attached', timeout: cfg.timeout }); + notify_game.status = 'claimed'; + } catch (e) { + notify_game.status = 'failed'; + const screenshotPath = path.resolve(cfg.dir.screenshots, 'epic-games', 'failed', `${game.offerId || game.pageSlug}_${filenamify(datetime())}.png`); + await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => { }); + console.error(' Failed to claim:', e.message); } - const cookies = await exchangeTokenForCookies(tokens.access_token); - writeFileSync(cookiesPath, JSON.stringify(cookies, null, 2)); - console.log('💾 Cookies saved to', cookiesPath); - return { bearerToken: tokens.access_token, cookies }; + return notify_game; }; // Ensure user is logged in @@ -194,7 +106,7 @@ const ensureLoggedIn = async (page, context) => { 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, { + await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded', timeout: cfg.login_timeout, }); @@ -302,70 +214,12 @@ const ensureLoggedIn = async (page, context) => { return user; }; -// Claim game function -const claimGame = async (page, game) => { - const purchaseUrl = `https://store.epicgames.com/${game.pageSlug}`; - console.log(`🎮 ${game.title} → ${purchaseUrl}`); - const notify_game = { title: game.title, url: purchaseUrl, status: 'failed' }; - - await page.goto(purchaseUrl, { waitUntil: 'networkidle' }); - - const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"]').first(); - await purchaseBtn.waitFor({ timeout: cfg.timeout }); - const btnText = (await purchaseBtn.textContent() || '').toLowerCase(); - - if (btnText.includes('library') || btnText.includes('owned')) { - notify_game.status = 'existed'; - return notify_game; - } - if (cfg.dryrun) { - notify_game.status = 'skipped'; - return notify_game; - } - - await purchaseBtn.click({ delay: 50 }); - - try { - await page.waitForSelector('#webPurchaseContainer iframe', { timeout: 15000 }); - const iframe = page.frameLocator('#webPurchaseContainer iframe'); - - if (cfg.eg_parentalpin) { - try { - await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 }); - await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin); - await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); - } catch { - // no PIN needed - } - } - - await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); - try { - await iframe.locator('button:has-text("I Accept")').click({ timeout: 5000 }); - } catch { - // not required - } - await page.locator('text=Thanks for your order!').waitFor({ state: 'attached', timeout: cfg.timeout }); - notify_game.status = 'claimed'; - } catch (e) { - notify_game.status = 'failed'; - const screenshotPath = path.resolve(cfg.dir.screenshots, 'epic-games', 'failed', `${game.offerId || game.pageSlug}_${filenamify(datetime())}.png`); - await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => { }); - console.error(' Failed to claim:', e.message); - } - - return notify_game; -}; - // Main function to claim Epic Games export const claimEpicGamesNew = async () => { - console.log('Starting Epic Games claimer (new mode, cookies + API)'); + console.log('Starting Epic Games claimer (new mode, browser-based)'); const db = await jsonDb('epic-games.json', {}); const notify_games = []; - const freeGames = await fetchFreeGamesAPI(); - console.log('Free games via API:', freeGames.map(g => g.pageSlug)); - const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, viewport: { width: cfg.width, height: cfg.height }, @@ -382,8 +236,19 @@ export const claimEpicGamesNew = async () => { const page = context.pages().length ? context.pages()[0] : await context.newPage(); await page.setViewportSize({ width: cfg.width, height: cfg.height }); + // Load cookies from file if available + if (existsSync(COOKIES_PATH)) { + try { + const cookies = JSON.parse(readFileSync(COOKIES_PATH, 'utf8')); + await context.addCookies(cookies); + console.log('✅ Cookies loaded from file'); + } catch (error) { + console.error('Failed to load cookies:', error); + } + } + // Use device auths if available (from legacy mode) - const deviceAuths = await getDeviceAuths(); + const deviceAuths = await getAccountAuth(); if (deviceAuths && cfg.eg_email) { const accountAuth = deviceAuths[cfg.eg_email]; if (accountAuth) { @@ -401,23 +266,13 @@ export const claimEpicGamesNew = async () => { let user; try { - const auth = await getValidAuth({ - otpKey: cfg.eg_otpkey, - reuseCookies: true, - cookiesPath: COOKIES_PATH, - }); - - if (auth.cookies?.length) { - await context.addCookies(auth.cookies); - console.log('✅ Cookies loaded:', auth.cookies.length); - } else { - console.log('⚠️ No cookies loaded; using manual login via browser.'); - } - await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); user = await ensureLoggedIn(page, context); db.data[user] ||= {}; + const freeGames = await fetchFreeGamesAPI(page); + console.log('Free games via API:', freeGames.map(g => g.pageSlug)); + for (const game of freeGames) { const result = await claimGame(page, game); notify_games.push(result); @@ -429,12 +284,14 @@ export const claimEpicGamesNew = async () => { }; } - await writeFileSync(COOKIES_PATH, JSON.stringify(await context.cookies(), null, 2)); + // Save cookies to file + const cookies = await context.cookies(); + writeFileSync(COOKIES_PATH, JSON.stringify(cookies, null, 2)); } catch (error) { process.exitCode ||= 1; console.error('--- Exception (new epic):'); console.error(error); - if (error.message && process.exitCode !== 130) notify(`epic-games (new) failed: ${error.message.split('\\n')[0]}`); + if (error.message && process.exitCode !== 130) notify(`epic-games (new) failed: ${error.message.split('\n')[0]}`); } finally { await db.write(); if (notify_games.filter(g => g.status === 'claimed' || g.status === 'failed').length) { @@ -447,7 +304,3 @@ export const claimEpicGamesNew = async () => { } await context.close(); }; - -export default claimEpicGamesNew; - -