From 3ccec68c33daabdf63665a31c7d5d4f8bece7cf6 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Apr 2023 19:19:56 +0200 Subject: [PATCH 1/8] urls from json, go to purchaseURL instead of click, got captcha --- epic-games.js | 57 ++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/epic-games.js b/epic-games.js index 38190831..f90a09c9 100644 --- a/epic-games.js +++ b/epic-games.js @@ -15,6 +15,19 @@ db.data ||= {}; handleSIGINT(); +// get current promotionalOffers from json instead of checking the website +// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // otherwise got UNABLE_TO_GET_ISSUER_CERT_LOCALLY +const promoJson = await (await fetch('https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions')).json(); // ?locale=en-US +const currentGames = promoJson.data.Catalog.searchStore.elements.filter(e => e.promotions?.promotionalOffers?.length); +const gameURL = e => `https://store.epicgames.com/p/${e.catalogNs.mappings[0].pageSlug}`; // gameURL(e.urlSlug) is wrong and leads to 404! +console.log('Free games:', currentGames.map(e => `${e.title} - ${gameURL(e)}`)); + +// TODO check if there are new games to claim before launching browser? https://github.com/vogler/free-games-claimer/issues/29 +// Options: +// 1. Check order history (https://www.epicgames.com/account/v2/payment/ajaxGetOrderHistory) - only contains the last 10 orders +// 2. Check epic-games.json - would need to know the logged in user for `cfg.dir.browser` +// However, this may not always speed up the process since a game may have already been claimed before. + // https://www.nopecha.com extension source from https://github.com/NopeCHA/NopeCHA/releases/tag/0.1.16 // const ext = path.resolve('nopecha'); // used in Chromium, currently not needed in Firefox @@ -99,18 +112,10 @@ try { console.log(`Signed in as ${user}`); db.data[user] ||= {}; - // Detect free games - const game_loc = page.locator('a:has(span:text-is("Free Now"))'); - await game_loc.last().waitFor(); - // clicking on `game_sel` sometimes led to a 404, see https://github.com/vogler/free-games-claimer/issues/25 - // debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking. - // Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions - // filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213 - const urlSlugs = await Promise.all((await game_loc.elementHandles()).map(a => a.getAttribute('href'))); - const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s); - console.log('Free games:', urls); - - for (const url of urls) { + // This URL will order all free games, but it will fail if some games have already been claimed: + // const purchaseURL = 'https://store.epicgames.com/purchase?' + currentGames.map(e => `offers=1-${e.namespace}-${e.id}`).join('&'); + for (const game of currentGames) { + const url = gameURL(game); await page.goto(url); // , { waitUntil: 'domcontentloaded' }); const btnText = await page.locator('//button[@data-testid="purchase-cta-button"][not(contains(.,"Loading"))]').first().innerText(); // barrier to block until page is loaded @@ -121,7 +126,8 @@ try { await page.waitForTimeout(2000); } - const title = await page.locator('h1').first().innerText(); + // const title = await page.locator('h1').first().innerText(); + const title = game.title; const game_id = page.url().split('/').pop(); db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! console.log('Current free game:', title); @@ -142,8 +148,10 @@ try { console.log(' Base game:', baseUrl); // await page.click('a:has-text("Overview")'); } else { // GET - console.log(' Not in library yet! Click GET.'); - await page.click('[data-testid="purchase-cta-button"]', { delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough + console.log(' Not in library yet! Claim!'); + // go to purchase of unclaimed game - https://github.com/vogler/free-games-claimer/issues/127 + const purchaseURL = `https://store.epicgames.com/purchase?offers=1-${game.namespace}-${game.id}`; + await page.goto(purchaseURL); // click Continue if 'Device not supported. This product is not compatible with your current device.' - avoided by Windows userAgent? page.click('button:has-text("Continue")').catch(_ => { }); // needed since change from Chromium to Firefox? @@ -155,23 +163,20 @@ try { await page.locator('button:has-text("Accept")').click(); }).catch(_ => { }); - // it then creates an iframe for the purchase - await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? - const iframe = page.frameLocator('#webPurchaseContainer iframe'); // skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46 TODO check games for account's region - if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) { + if (await page.locator(':has-text("unavailable in your region")').count() > 0) { console.error(' This product is unavailable in your region!'); db.data[user][game_id].status = notify_game.status = 'unavailable-in-region'; continue; } - iframe.locator('.payment-pin-code').waitFor().then(async () => { + page.locator('.payment-pin-code').waitFor().then(async () => { if (!cfg.eg_parentalpin) { console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); } - await iframe.locator('input.payment-pin-code__input').first().type(cfg.eg_parentalpin); - await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); + await page.locator('input.payment-pin-code__input').first().type(cfg.eg_parentalpin); + await page.locator('button:has-text("Continue")').click({ delay: 11 }); }).catch(_ => { }); if (cfg.debug) await page.pause(); @@ -181,14 +186,14 @@ try { } // Playwright clicked before button was ready to handle event, https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1474346591 - await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); + await page.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); // I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872 - const btnAgree = iframe.locator('button:has-text("I Agree")'); + const btnAgree = page.locator('button:has-text("I Agree")'); btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' try { // context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s? - const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe'); + const captcha = page.locator('#h_captcha_challenge_checkout_free_prod iframe'); captcha.waitFor().then(async () => { // don't await, since element may not be shown // console.info(' Got hcaptcha challenge! NopeCHA extension will likely solve it.') console.error(' Got hcaptcha challenge! Lost trust due to too many login attempts? You can solve the captcha in the browser or get a new IP address.') @@ -229,4 +234,4 @@ try { } } if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); -await context.close(); \ No newline at end of file +await context.close(); From 254b674c6563912029192b80ffea184964dfe685 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Apr 2023 19:23:13 +0200 Subject: [PATCH 2/8] mention error if game is already claimed --- epic-games.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/epic-games.js b/epic-games.js index f90a09c9..7b2d879e 100644 --- a/epic-games.js +++ b/epic-games.js @@ -191,6 +191,9 @@ try { // I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872 const btnAgree = page.locator('button:has-text("I Agree")'); btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' + + // May fail if game is already claimed with text 'Sorry, there is an error with your cart and we cannot complete the purchase. Please close this window and check your cart list.' + try { // context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s? const captcha = page.locator('#h_captcha_challenge_checkout_free_prod iframe'); From 47be85f45cb8f2aa5c17350be152f836cdf3e5ad Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 11 May 2023 16:31:48 +0200 Subject: [PATCH 3/8] eg: EG_COUNTRY to set country of account to avoid unavailable-in-region --- README.md | 1 + config.js | 1 + epic-games.js | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c10d7264..3afe9f1d 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Available options/variables and their default values: | EG_PASSWORD | | Epic Games password for login. Overrides PASSWORD. | | EG_OTPKEY | | Epic Games MFA OTP key. | | EG_PARENTALPIN | | Epic Games Parental Controls PIN. | +| EG_COUNTRY | US | Epic Games [country of account](https://www.epicgames.com/account/personal). Set to avoid unavailable-in-region. | | PG_EMAIL | | Prime Gaming email for login. Overrides EMAIL. | | PG_PASSWORD | | Prime Gaming password for login. Overrides PASSWORD. | | PG_OTPKEY | | Prime Gaming MFA OTP key. | diff --git a/config.js b/config.js index 33016cae..ef3ed3f5 100644 --- a/config.js +++ b/config.js @@ -27,6 +27,7 @@ export const cfg = { eg_password: process.env.EG_PASSWORD || process.env.PASSWORD, eg_otpkey: process.env.EG_OTPKEY, eg_parentalpin: process.env.EG_PARENTALPIN, + eg_country: process.env.EG_COUNTRY || 'US', // This should fit your account's country since sometimes there are replacements for games that are unavailable-in-region. See country/region under https://www.epicgames.com/account/personal and use its two-letter country code. // auth prime-gaming pg_email: process.env.PG_EMAIL || process.env.EMAIL, pg_password: process.env.PG_PASSWORD || process.env.PASSWORD, diff --git a/epic-games.js b/epic-games.js index 7b2d879e..a9e2c271 100644 --- a/epic-games.js +++ b/epic-games.js @@ -17,7 +17,7 @@ handleSIGINT(); // get current promotionalOffers from json instead of checking the website // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // otherwise got UNABLE_TO_GET_ISSUER_CERT_LOCALLY -const promoJson = await (await fetch('https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions')).json(); // ?locale=en-US +const promoJson = await (await fetch(`https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?country=${cfg.eg_country}`)).json(); // ?locale=en-US const currentGames = promoJson.data.Catalog.searchStore.elements.filter(e => e.promotions?.promotionalOffers?.length); const gameURL = e => `https://store.epicgames.com/p/${e.catalogNs.mappings[0].pageSlug}`; // gameURL(e.urlSlug) is wrong and leads to 404! console.log('Free games:', currentGames.map(e => `${e.title} - ${gameURL(e)}`)); From 32922571720e400fde592cc61253bce478652333 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 11 May 2023 17:51:29 +0200 Subject: [PATCH 4/8] eg: try to wait for response confirm-order instead of text on empty page --- epic-games.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/epic-games.js b/epic-games.js index 23c0eebf..4a5bb9d2 100644 --- a/epic-games.js +++ b/epic-games.js @@ -193,6 +193,9 @@ try { continue; } + // After successful order using the `purchaseURL`-method, the page is just empty, without any 'Thanks for your order', so we wait for the response of their API. Note: no await, and start waiting before final click to 'Place Order'. + const r = page.waitForResponse(r => r.url().startsWith('https://payment-website-pci.ol.epicgames.com/purchase/confirm-order')); + // Playwright clicked before button was ready to handle event, https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1474346591 await page.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); @@ -214,7 +217,14 @@ try { // console.info(' Saved a screenshot of hcaptcha challenge to', p); // console.error(' Got hcaptcha challenge. To avoid it, get a link from https://www.hcaptcha.com/accessibility'); // TODO save this link in config and visit it daily to set accessibility cookie to avoid captcha challenge? }).catch(_ => { }); // may time out if not shown - await page.waitForSelector('text=Thanks for your order!'); + // await page.waitForSelector('text=Thanks for your order!'); // not shown for order via `purchaseURL` + const rt = await (await r).text(); // TODO blocks if not claimed? + const rj = JSON.parse(rt); + if (rj?.receiptResponse?.orderStatus != 'COMPLETED') { + console.error('Unexpected confirm-order response. Message:', rj.message); + console.log(rj); + continue; + } db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time console.log(' Claimed successfully!'); From 02ace424f3966fac00a8acef05e4ca0406490871 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 11 May 2023 18:25:04 +0200 Subject: [PATCH 5/8] eg: fix gameURL for add-ons - old `e.catalogNs.mappings[0].pageSlug` -> base game - https://store.epicgames.com/en-US/p/the-sims-4 - new `e.offerMappings[0].pageSlug` -> add-on - https://store.epicgames.com/en-US/p/the-sims-4--the-daring-lifestyle-bundle --- epic-games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epic-games.js b/epic-games.js index 4a5bb9d2..78cca865 100644 --- a/epic-games.js +++ b/epic-games.js @@ -19,7 +19,7 @@ handleSIGINT(); // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // otherwise got UNABLE_TO_GET_ISSUER_CERT_LOCALLY const promoJson = await (await fetch(`https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?country=${cfg.eg_country}`)).json(); // ?locale=en-US const currentGames = promoJson.data.Catalog.searchStore.elements.filter(e => e.promotions?.promotionalOffers?.length); -const gameURL = e => `https://store.epicgames.com/p/${e.catalogNs.mappings[0].pageSlug}`; // gameURL(e.urlSlug) is wrong and leads to 404! +const gameURL = e => `https://store.epicgames.com/p/${e.offerMappings[0].pageSlug}`; // e.urlSlug may be wrong and lead to 404, e.catalogNs.mappings[0].pageSlug leads to base game for add-ons! console.log('Free games:', currentGames.map(e => `${e.title} - ${gameURL(e)}`)); // TODO check if there are new games to claim before launching browser? https://github.com/vogler/free-games-claimer/issues/29 From 1d1f95d1abaa862bc07b6849ca611f78a41184b7 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 18 May 2023 16:40:41 +0200 Subject: [PATCH 6/8] eg: move screenshot up before status checks since blank page after purchaseURL --- epic-games.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/epic-games.js b/epic-games.js index 78cca865..1db45090 100644 --- a/epic-games.js +++ b/epic-games.js @@ -141,6 +141,9 @@ try { const notify_game = { title, url, status: 'failed' }; notify_games.push(notify_game); // status is updated below + const p = path.resolve(cfg.dir.screenshots, 'epic-games', `${game_id}.png`); + if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... + if (btnText.toLowerCase() == 'in library') { console.log(' Already in library! Nothing to claim.'); notify_game.status = 'existed'; @@ -238,9 +241,6 @@ try { db.data[user][game_id].status = 'failed'; } notify_game.status = db.data[user][game_id].status; // claimed or failed - - const p = path.resolve(cfg.dir.screenshots, 'epic-games', `${game_id}.png`); - if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... } } } catch (error) { From 4e8e8ee884adb254c8fcb63ecfb968f372f33fd7 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 25 May 2023 14:19:17 +0200 Subject: [PATCH 7/8] eg: `e.offerMappings` is `[]` for Death Stranding, instead `e.productSlug` fits --- epic-games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epic-games.js b/epic-games.js index 1db45090..de50f61d 100644 --- a/epic-games.js +++ b/epic-games.js @@ -19,7 +19,7 @@ handleSIGINT(); // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // otherwise got UNABLE_TO_GET_ISSUER_CERT_LOCALLY const promoJson = await (await fetch(`https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?country=${cfg.eg_country}`)).json(); // ?locale=en-US const currentGames = promoJson.data.Catalog.searchStore.elements.filter(e => e.promotions?.promotionalOffers?.length); -const gameURL = e => `https://store.epicgames.com/p/${e.offerMappings[0].pageSlug}`; // e.urlSlug may be wrong and lead to 404, e.catalogNs.mappings[0].pageSlug leads to base game for add-ons! +const gameURL = e => `https://store.epicgames.com/p/${e.productSlug || e.offerMappings[0].pageSlug}`; // e.urlSlug may be wrong and lead to 404, e.catalogNs.mappings[0].pageSlug leads to base game for add-ons! console.log('Free games:', currentGames.map(e => `${e.title} - ${gameURL(e)}`)); // TODO check if there are new games to claim before launching browser? https://github.com/vogler/free-games-claimer/issues/29 From 5c50aa8de7389cef35875da46ef85b8ae550b3d6 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 1 Jun 2023 16:58:15 +0200 Subject: [PATCH 8/8] eg: print purchaseURL, #127 --- epic-games.js | 1 + 1 file changed, 1 insertion(+) diff --git a/epic-games.js b/epic-games.js index de50f61d..6480146e 100644 --- a/epic-games.js +++ b/epic-games.js @@ -161,6 +161,7 @@ try { console.log(' Not in library yet! Claim!'); // go to purchase of unclaimed game - https://github.com/vogler/free-games-claimer/issues/127 const purchaseURL = `https://store.epicgames.com/purchase?offers=1-${game.namespace}-${game.id}`; + console.log(' purchaseURL:', purchaseURL); await page.goto(purchaseURL); // click Continue if 'Device not supported. This product is not compatible with your current device.' - avoided by Windows userAgent?