diff --git a/epic-games.js b/epic-games.js index bbf000c..d7eb80d 100644 --- a/epic-games.js +++ b/epic-games.js @@ -48,9 +48,18 @@ if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it -// some debug info about the page (screen dimensions, user agent, platform) -// eslint-disable-next-line no-undef -if (cfg.debug) console.debug(await page.evaluate(() => [(({ width, height, availWidth, availHeight }) => ({ width, height, availWidth, availHeight }))(window.screen), navigator.userAgent, navigator.platform, navigator.vendor])); // deconstruct screen needed since `window.screen` prints {}, `window.screen.toString()` '[object Screen]', and can't use some pick function without defining it on `page` +// some debug info about the page (screen dimensions, user agent) +if (cfg.debug) { + // eslint-disable-next-line no-undef + const debugInfo = await page.evaluate(() => { + const { width, height, availWidth, availHeight } = window.screen; + return { + screen: { width, height, availWidth, availHeight }, + userAgent: navigator.userAgent, + }; + }); + console.debug(debugInfo); +} if (cfg.debug_network) { // const filter = _ => true; const filter = r => r.url().includes('store.epicgames.com'); @@ -90,9 +99,8 @@ try { } }; const email = cfg.eg_email || await prompt({ message: 'Enter email' }); - if (!email) await notifyBrowserLogin(); - else { - void (async () => { + if (email) { + const watchCaptchaChallenge = async () => { try { await page.waitForSelector('.h_captcha_challenge iframe', { timeout: 15000 }); console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); @@ -100,24 +108,25 @@ try { } catch { return; } - })(); - void (async () => { + }; + const watchCaptchaIncorrect = async () => { try { await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 }); console.error('Incorrect response for captcha!'); } catch { return; } - })(); + }; + watchCaptchaChallenge(); + watchCaptchaIncorrect(); await page.fill('#email', email); - const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' })); - if (!password) await notifyBrowserLogin(); - else { + const password = cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' }); + if (password) { await page.fill('#password', password); await page.click('button[type="submit"]'); - } + } else await notifyBrowserLogin(); const error = page.locator('#form-error-message'); - void (async () => { + const watchLoginError = async () => { try { await error.waitFor({ timeout: 15000 }); console.error('Login error:', await error.innerText()); @@ -125,8 +134,8 @@ try { } catch { return; } - })(); - void (async () => { + }; + const watchMfaStep = async () => { try { await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout }); console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...'); @@ -136,8 +145,10 @@ try { } catch { return; } - })(); - } + }; + watchLoginError(); + watchMfaStep(); + } else await notifyBrowserLogin(); await page.waitForURL(URL_CLAIM); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); } @@ -223,14 +234,13 @@ try { console.log(' Base game:', baseUrl); // await page.click('a:has-text("Overview")'); // re-add original add-on to queue after base game - urls.push(baseUrl); // add base game to the list of games to claim - urls.push(url); // add add-on itself again + urls.push(baseUrl, url); // add base game to the list of games to claim and re-add add-on itself } else { // GET console.log(' Not in library yet! Click', btnText); await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough // Accept End User License Agreement (only needed once) - void (async () => { + const acceptEulaIfShown = async () => { try { await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 }); console.log(' Accept End User License Agreement (only needed once)'); @@ -239,7 +249,8 @@ try { } catch { return; } - })(); + }; + acceptEulaIfShown(); // it then creates an iframe for the purchase await page.waitForSelector('#webPurchaseContainer iframe'); @@ -252,7 +263,7 @@ try { continue; } - void (async () => { + const enterParentalPinIfNeeded = async () => { try { await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 }); if (!cfg.eg_parentalpin) { @@ -264,7 +275,8 @@ try { } catch { return; } - })(); + }; + enterParentalPinIfNeeded(); if (cfg.debug) await page.pause(); if (cfg.dryrun) { @@ -279,18 +291,19 @@ try { // 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 Accept")'); - void (async () => { + const acceptIfRequired = async () => { try { await btnAgree.waitFor({ timeout: 10000 }); await btnAgree.click(); } catch { return; } - })(); // EU: wait for and click 'I Agree' + }; // EU: wait for and click 'I Agree' + acceptIfRequired(); 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'); - void (async () => { + const watchCaptchaChallenge = async () => { try { await captcha.waitFor({ timeout: 10000 }); 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.'); @@ -298,8 +311,8 @@ try { } catch { return; } - })(); // may time out if not shown - void (async () => { + }; // may time out if not shown + const watchCaptchaFailure = async () => { try { await iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor({ timeout: 10000 }); console.error(' Failed to challenge captcha, please try again later.'); @@ -307,7 +320,9 @@ try { } catch { return; } - })(); + }; + watchCaptchaChallenge(); + watchCaptchaFailure(); await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time diff --git a/eslint.config.js b/eslint.config.js index 48b1cbc..3c99bf5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,7 +11,7 @@ export default [ { ignores: ['data/**'], }, - js.configs.recommended, // TODO still needed? + js.configs.recommended, { // files: ['*.js'], languageOptions: { diff --git a/gog.js b/gog.js index 5f0abad..767fec1 100644 --- a/gog.js +++ b/gog.js @@ -60,7 +60,7 @@ try { await iframe.locator('#login_username').fill(email); await iframe.locator('#login_password').fill(password); await iframe.locator('#login_login').click(); - void (async () => { + const handleTwoFactor = async () => { try { await iframe.locator('form[name=second_step_authentication]').waitFor({ timeout: 15000 }); console.log('Two-Step Verification - Enter security code'); @@ -72,8 +72,8 @@ try { } catch { return; } - })(); - void (async () => { + }; + const watchInvalidCaptcha = async () => { try { await iframe.locator('text=Invalid captcha').waitFor({ timeout: 15000 }); console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); @@ -81,7 +81,9 @@ try { } catch { return; } - })(); + }; + handleTwoFactor(); + watchInvalidCaptcha(); await page.waitForSelector('#menuUsername'); } else { console.log('Waiting for you to login in the browser.'); @@ -100,7 +102,8 @@ try { db.data[user] ||= {}; const banner = page.locator('#giveaway'); - if (!await banner.count()) { + const hasGiveaway = await banner.count(); + if (!hasGiveaway) { console.log('Currently no free giveaway!'); } else { const text = await page.locator('.giveaway__content-header').innerText(); diff --git a/prime-gaming.js b/prime-gaming.js index c881f79..47d7c70 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -92,7 +92,11 @@ try { '[data-a-target="user-dropdown-first-name-text"]', '[data-testid="user-dropdown-first-name-text"]', ].map(s => page.waitForSelector(s))); - page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(() => { }); // to not waste screen space when non-headless; could be flaky + try { + await page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space when non-headless; could be flaky + } catch { + // ignore if banner not present + } while (await page.locator('button:has-text("Sign in"), button:has-text("Anmelden")').count() > 0) { console.error('Not signed in anymore.'); await page.click('button:has-text("Sign in")'); @@ -119,7 +123,11 @@ try { } catch { // navigation ok } - handleMFA(page).catch(() => {}); + try { + await handleMFA(page); + } catch { + // ignore MFA watcher errors + } } else { console.log('Waiting for you to login in the browser.'); await notify('prime-gaming: no longer signed in and not enough options set for automatic login.'); @@ -297,7 +305,6 @@ try { return [p, isNew]; }; const skipBasedOnTime = async url => { - // console.log(' Checking time left for game:', url); const [p, isNew] = await sameOrNewPage(url); const dueDateLoc = p.locator('.availability-date .tw-bold, [data-testid="availability-end-date"], [data-test-selector="availability-end-date"]'); if (!await dueDateLoc.count()) { @@ -321,7 +328,10 @@ try { console.log('Current free game:', chalk.blue(title)); if (cfg.pg_timeLeft && url && await skipBasedOnTime(url)) continue; if (cfg.dryrun) continue; - if (cfg.interactive && !await confirm()) continue; + if (cfg.interactive) { + const confirmed = await confirm(); + if (!confirmed) continue; + } await card.handle.locator('.tw-button:has-text("Claim"), .tw-button:has-text("Get"), button:has-text("Claim"), button:has-text("Get")').first().click(); db.data[user][title] ||= { title, time: datetime(), url, store: 'internal' }; notify_games.push({ title, status: 'claimed', url }); @@ -336,8 +346,6 @@ try { if (!url) continue; external_info.push({ title, url }); } - // external_info = [ { title: 'Fallout 76 (XBOX)', url: 'https://gaming.amazon.com/fallout-76-xbox-fgwp/dp/amzn1.pg.item.9fe17d7b-b6c2-4f58-b494-cc4e79528d0b?ingress=amzn&ref_=SM_Fallout76XBOX_S01_FGWP_CRWN' } ]; - const clickCTA = async p => { const candidates = [ p.locator('button[data-a-target="buy-box_call-to-action"]').first(), @@ -353,14 +361,14 @@ try { if (await c.count()) { try { await c.waitFor({ state: 'visible', timeout: 5000 }); - if (!await c.isEnabled()) { + const enabled = await c.isEnabled(); + if (enabled) await c.click(); + else { await c.evaluate(el => { el.disabled = false; el.removeAttribute('disabled'); el.click(); }); - } else { - await c.click(); } return true; } catch { @@ -442,7 +450,10 @@ try { } if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue; if (cfg.dryrun) continue; - if (cfg.interactive && !await confirm()) continue; + if (cfg.interactive) { + const confirmed = await confirm(); + if (!confirmed) continue; + } await clickCTA(page); await Promise.any([ page.waitForSelector('.thank-you-title:has-text("Success")', { timeout: cfg.timeout }).catch(() => {}), @@ -499,13 +510,9 @@ try { const page2 = await context.newPage(); await page2.goto(redeem[store], { waitUntil: 'domcontentloaded' }); if (store == 'gog.com') { - // await page.goto(`https://redeem.gog.com/v1/bonusCodes/${code}`); // {"reason":"Invalid or no captcha"} await page2.fill('#codeInput', code); - // wait for responses before clicking on Continue and then Redeem - // first there are requests with OPTIONS and GET to https://redeem.gog.com/v1/bonusCodes/XYZ?language=de-DE const r1 = page2.waitForResponse(r => r.request().method() == 'GET' && r.url().startsWith('https://redeem.gog.com/')); await page2.click('[type="submit"]'); // click Continue - // console.log(await page2.locator('.warning-message').innerText()); // does not exist if there is no warning const r1t = await (await r1).text(); const reason = JSON.parse(r1t).reason; // {"reason":"Invalid or no captcha"} @@ -520,14 +527,12 @@ try { } else if (reason == 'code_not_found') { redeem_action = 'redeem (not found)'; console.error(' Code was not found!'); - } else { // unknown state; keep info log for later analysis - redeem_action = 'redeemed?'; - // console.log(' Redeemed successfully? Please report your Responses (if new) in https://github.com/vogler/free-games-claimer/issues/5'); - console.debug(` Response 1: ${r1t}`); - // then after the click on Redeem there is a POST request which should return {} if claimed successfully - const r2 = page2.waitForResponse(r => r.request().method() == 'POST' && r.url().startsWith('https://redeem.gog.com/')); - await page2.click('[type="submit"]'); // click Redeem - const r2t = await (await r2).text(); + } else { // unknown state; keep info log for later analysis + redeem_action = 'redeemed?'; + console.debug(` Response 1: ${r1t}`); + const r2 = page2.waitForResponse(r => r.request().method() == 'POST' && r.url().startsWith('https://redeem.gog.com/')); + await page2.click('[type="submit"]'); // click Redeem + const r2t = await (await r2).text(); const reason2 = JSON.parse(r2t).reason; if (r2t == '{}') { redeem_action = 'redeemed'; @@ -543,7 +548,6 @@ try { } } else if (store == 'microsoft store' || store == 'xbox') { console.error(` Redeem on ${store} is experimental!`); - // await page2.pause(); if (page2.url().startsWith('https://login.')) { console.error(' Not logged in! Please redeem the code above manually. You can now login in the browser for next time. Waiting for 60s.'); await page2.waitForTimeout(60 * 1000); @@ -554,9 +558,7 @@ try { await input.waitFor(); await input.fill(code); const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/PrepareRedeem')); - // console.log(await page2.locator('.redeem_code_error').innerText()); const rt = await (await r).text(); - // {"code":"NotFound","data":[],"details":[],"innererror":{"code":"TokenNotFound",... const j = JSON.parse(rt); const reason = j?.events?.cart.length && j.events.cart[0]?.data?.reason; if (reason == 'TokenNotFound') { @@ -582,14 +584,12 @@ try { } } } else if (store == 'legacy games') { - // await page2.pause(); await page2.fill('[name=coupon_code]', code); await page2.fill('[name=email]', cfg.lg_email); await page2.fill('[name=email_validate]', cfg.lg_email); await page2.uncheck('[name=newsletter_sub]'); await page2.click('[type="submit"]'); try { - // await page2.waitForResponse(r => r.url().startsWith('https://promo.legacygames.com/promotion-processing/order-management.php')); // status code 302 await page2.waitForSelector('h2:has-text("Thanks for redeeming")'); redeem_action = 'redeemed'; db.data[user][title].status = 'claimed and redeemed'; @@ -609,15 +609,16 @@ try { } else { notify_game.status = `claimed on ${store}`; db.data[user][title].status = 'claimed'; - } - // save screenshot of potential code just in case - await page.screenshot({ path: screenshot('external', `${filenamify(title)}.png`), fullPage: true }); - // console.info(' Saved a screenshot of page to', p); } - // await page.pause(); + await page.screenshot({ path: screenshot('external', `${filenamify(title)}.png`), fullPage: true }); + } } await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); - page.click('button[data-type="Game"]').catch(() => {}); + try { + await page.click('button[data-type="Game"]'); + } catch { + // ignore if filter already selected + } if (notify_games.length && games) { // make screenshot of all games if something was claimed and list exists const p = screenshot(`${filenamify(datetime())}.png`); @@ -663,15 +664,28 @@ try { await page.goto(url, { waitUntil: 'domcontentloaded' }); // most games have a button 'Get in-game content' // epic-games: Fall Guys: Claim -> Continue -> Go to Epic Games (despite account linked and logged into epic-games) -> not tied to account but via some cookie? - await Promise.any([page.click('.tw-button:has-text("Get in-game content")'), page.click('.tw-button:has-text("Claim your gift")'), page.click('.tw-button:has-text("Claim")').then(() => page.click('button:has-text("Continue")'))]); - page.click('button:has-text("Continue")').catch(() => { }); + const claimOptions = [ + page.click('.tw-button:has-text("Get in-game content")'), + page.click('.tw-button:has-text("Claim your gift")'), + (async () => { + await page.click('.tw-button:has-text("Claim")'); + await page.click('button:has-text("Continue")').catch(() => {}); + })(), + ]; + await Promise.any(claimOptions); + try { + await page.click('button:has-text("Continue")'); + } catch { + // continue button not always present + } const linkAccountButton = page.locator('[data-a-target="LinkAccountButton"]'); let unlinked_store; if (await linkAccountButton.count()) { unlinked_store = await linkAccountButton.first().getAttribute('aria-label'); console.debug(' LinkAccountButton label:', unlinked_store); - const match = unlinked_store.match(/Link (.*) account/); - if (match && match.length == 2) unlinked_store = match[1]; + const match = unlinked_store?.match(/Link (.*) account/); + const extracted = match?.[1]; + if (extracted) unlinked_store = extracted; } else if (await page.locator('text=Link game account').count()) { // epic-games only? console.error(' Missing account linking (epic-games specific button?):', await page.locator('button[data-a-target="gms-cta"]').innerText()); // track account-linking UI drift unlinked_store = 'epic-games'; @@ -685,9 +699,7 @@ try { console.log(' Code to redeem game:', chalk.blue(code)); db.data[user][title].code = code; db.data[user][title].status = 'claimed'; - // notify_game.status = `${redeem_action} ${code} on ${store}`; } - // await page.pause(); } catch (error) { console.error(error); } finally { diff --git a/test/sigint-enquirer-raw.js b/test/sigint-enquirer-raw.js index baf1e1c..f9f365c 100644 --- a/test/sigint-enquirer-raw.js +++ b/test/sigint-enquirer-raw.js @@ -6,9 +6,9 @@ handleSIGINT(); console.log('hello'); console.error('hello error'); try { - let i = await prompt(); // SIGINT no longer handled if this is executed - i = await prompt(); // SIGINT no longer handled if this is executed - console.log('value:', i); + const first = await prompt(); // SIGINT no longer handled if this is executed + const second = await prompt(); // SIGINT no longer handled if this is executed + console.log('values:', first, second); setTimeout(() => console.log('timeout 3s'), 3000); } catch (e) { process.exitCode ||= 1; diff --git a/unrealengine.js b/unrealengine.js index 1098a28..6eb7d79 100644 --- a/unrealengine.js +++ b/unrealengine.js @@ -59,7 +59,7 @@ try { await page.click('button[type="submit"]'); await page.fill('#password', password); await page.click('button[type="submit"]'); - void (async () => { + const watchCaptchaDuringLogin = async () => { try { await page.waitForSelector('#h_captcha_challenge_login_prod iframe', { timeout: 15000 }); console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); @@ -67,8 +67,8 @@ try { } catch { return; } - })(); - void (async () => { + }; + const watchMfa = async () => { try { await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout }); console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...'); @@ -78,7 +78,9 @@ try { } catch { return; } - })(); + }; + watchCaptchaDuringLogin(); + watchMfa(); } else { console.log('Waiting for you to login in the browser.'); await notify('unrealengine: no longer signed in and not enough options set for automatic login.'); @@ -125,7 +127,7 @@ try { } ids.push(id); } - if (!ids.length) { + if (ids.length === 0) { console.log('Nothing to claim'); } else { await page.waitForTimeout(2000); @@ -142,7 +144,7 @@ try { await page.locator('button.checkout').click(); console.log('Click checkout'); // maybe: Accept End User License Agreement - void (async () => { + const acceptEulaIfPresent = async () => { try { await page.locator('[name=accept-label]').check({ timeout: 10000 }); console.log('Accept End User License Agreement'); @@ -150,7 +152,8 @@ try { } catch { return; } - })(); + }; + acceptEulaIfPresent(); await page.waitForSelector('#webPurchaseContainer iframe'); const iframe = page.frameLocator('#webPurchaseContainer iframe'); @@ -166,25 +169,27 @@ try { // 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")'); - void (async () => { + const acceptIfRequired = async () => { try { await btnAgree.waitFor({ timeout: 10000 }); await btnAgree.click(); } catch { return; } - })(); // EU: wait for and click 'I Agree' + }; // EU: wait for and click 'I Agree' + acceptIfRequired(); 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'); - void (async () => { + const watchCaptchaChallenge = async () => { try { await captcha.waitFor({ timeout: 10000 }); 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.'); } catch { return; } - })(); // may time out if not shown + }; // may time out if not shown + watchCaptchaChallenge(); await page.waitForSelector('text=Thank you'); for (const id of ids) { db.data[user][id].status = 'claimed';