diff --git a/epic-games.js b/epic-games.js index b12b35a..8253881 100644 --- a/epic-games.js +++ b/epic-games.js @@ -29,17 +29,13 @@ if (existsSync(browserPrefs)) { const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, viewport: { width: cfg.width, height: cfg.height }, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? - // userAgent firefox (macOS): Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0 - // userAgent firefox (docker): Mozilla/5.0 (X11; Linux aarch64; rv:109.0) Gecko/20100101 Firefox/115.0 + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // Windows UA avoids "device not supported"; update when browser version changes locale: 'en-US', // ignore OS locale to be sure to have english text for locators recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 recordHar: cfg.record ? { path: `data/record/eg-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved // user settings for firefox have to be put in $BROWSER_DIR/user.js - args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions - // '-kiosk', - ], + args: [], // https://wiki.mozilla.org/Firefox/CommandLineOptions }); handleSIGINT(context); @@ -50,7 +46,7 @@ await stealth(context); 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 }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it +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 @@ -76,8 +72,6 @@ try { if (cfg.time) console.timeEnd('startup'); if (cfg.time) console.time('login'); - // page.click('button:has-text("Accept All Cookies")').catch(_ => { }); // Not needed anymore since we set the cookie above. Clicking this did not always work since the message was animated in too slowly. - while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') { console.error('Not signed in anymore. Please login in the browser or here in the terminal.'); if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`); @@ -98,16 +92,20 @@ try { const email = cfg.eg_email || await prompt({ message: 'Enter email' }); if (!email) await notifyBrowserLogin(); else { - // await page.click('text=Sign in with Epic Games'); - page.waitForSelector('.h_captcha_challenge iframe').then(async () => { - 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.'); - await notify('epic-games: got captcha during login. Please check.'); - }).catch(_ => { }); - page.waitForSelector('p:has-text("Incorrect response.")').then(async () => { - console.error('Incorrect response for captcha!'); - }).catch(_ => { }); + void (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.'); + await notify('epic-games: got captcha during login. Please check.'); + } catch {} + })(); + void (async () => { + try { + await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 }); + console.error('Incorrect response for captcha!'); + } catch {} + })(); await page.fill('#email', email); - // await page.click('button[type="submit"]'); login was split in two steps for some time, now email and password are on the same form again const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' })); if (!password) await notifyBrowserLogin(); else { @@ -115,18 +113,22 @@ try { await page.click('button[type="submit"]'); } const error = page.locator('#form-error-message'); - error.waitFor().then(async () => { - console.error('Login error:', await error.innerText()); - console.log('Please login in the browser!'); - }).catch(_ => { }); - // handle MFA, but don't await it - page.waitForURL('**/id/login/mfa**').then(async () => { - 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 ...'); - // TODO locator for text (email or app?) - 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!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); - await page.click('button[type="submit"]'); - }).catch(_ => { }); + void (async () => { + try { + await error.waitFor({ timeout: 15000 }); + console.error('Login error:', await error.innerText()); + console.log('Please login in the browser!'); + } catch {} + })(); + void (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 ...'); + 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!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them + await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); + await page.click('button[type="submit"]'); + } catch {} + })(); } await page.waitForURL(URL_CLAIM); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); @@ -141,7 +143,7 @@ try { const game_loc = page.locator('a:has(span:text-is("Free Now"))'); await game_loc.last().waitFor().catch(_ => { // rarely there are no free games available -> catch Timeout - // TODO would be better to wait for alternative like 'coming soon' instead of waiting for timeout + // waiting for timeout; alternative would be waiting for "coming soon" // see https://github.com/vogler/free-games-claimer/issues/210#issuecomment-1727420943 console.error('Seems like currently there are no free games available in your region...'); // urls below should then be an empty list @@ -208,36 +210,31 @@ try { console.log(' Requires base game! Nothing to claim.'); notify_game.status = 'requires base game'; db.data[user][game_id].status ||= 'failed:requires-base-game'; - // TODO claim base game if it is free + // if base game is free, add to queue as well const baseUrl = 'https://store.epicgames.com' + await page.locator('a:has-text("Overview")').getAttribute('href'); console.log(' Base game:', baseUrl); // await page.click('a:has-text("Overview")'); - // TODO handle this via function call for base game above since this will never terminate if DRYRUN=1 + // 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 } 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 - // 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? - - // click 'Yes, buy now' if 'This edition contains something you already have. Still interested?' - page.click('button:has-text("Yes, buy now")').catch(_ => { }); - // Accept End User License Agreement (only needed once) - page.locator(':has-text("end user license agreement")').waitFor().then(async () => { - console.log(' Accept End User License Agreement (only needed once)'); - console.log(page.innerHTML); - console.log('Please report the HTML above here: https://github.com/vogler/free-games-claimer/issues/371'); - await page.locator('input#agree').check(); // TODO Bundle: got stuck here; likely unrelated to bundle and locator just changed: https://github.com/vogler/free-games-claimer/issues/371 - await page.locator('button:has-text("Accept")').click(); - }).catch(_ => { }); + void (async () => { + try { + await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 }); + console.log(' Accept End User License Agreement (only needed once)'); + await page.locator('input#agree').check(); + await page.locator('button:has-text("Accept")').click(); + } catch {} + })(); // it then creates an iframe for the purchase - await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? + await page.waitForSelector('#webPurchaseContainer iframe'); 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 + // skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46 if (await iframe.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'; @@ -245,14 +242,17 @@ try { continue; } - iframe.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().pressSequentially(cfg.eg_parentalpin); - await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); - }).catch(_ => { }); + void (async () => { + try { + await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 }); + 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().pressSequentially(cfg.eg_parentalpin); + await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); + } catch {} + })(); if (cfg.debug) await page.pause(); if (cfg.dryrun) { @@ -267,27 +267,30 @@ 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")'); - btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' + void (async () => { + try { + await btnAgree.waitFor({ timeout: 10000 }); + await 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'); - 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.'); - // await notify(`epic-games: got captcha challenge right before claim of ${title}. Use VNC to solve it manually.`); // TODO not all apprise services understand HTML: https://github.com/vogler/free-games-claimer/pull/417 - await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`); - // TODO could even create purchase URL, see https://github.com/vogler/free-games-claimer/pull/130 - // await page.waitForTimeout(2000); - // const p = path.resolve(cfg.dir.screenshots, 'epic-games', 'captcha', `${filenamify(datetime())}.png`); - // await captcha.screenshot({ path: p }); - // 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 - iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor().then(async () => { - console.error(' Failed to challenge captcha, please try again later.'); - await notify('epic-games: failed to challenge captcha. Please check.'); - }).catch(_ => { }); - await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here, but normal game now as well + void (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.'); + await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`); + } catch {} + })(); // may time out if not shown + void (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.'); + await notify('epic-games: failed to challenge captcha. Please check.'); + } catch {} + })(); + 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 console.log(' Claimed successfully!'); diff --git a/gog.js b/gog.js index 67da726..9b8b2b8 100644 --- a/gog.js +++ b/gog.js @@ -31,7 +31,7 @@ handleSIGINT(context); 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 }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it +await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it const notify_games = []; let user; @@ -47,7 +47,7 @@ try { console.error('Not signed in anymore.'); await signIn.click(); // it then creates an iframe for the login - await page.waitForSelector('#GalaxyAccountsFrameContainer iframe'); // TODO needed? + await page.waitForSelector('#GalaxyAccountsFrameContainer iframe'); const iframe = page.frameLocator('#GalaxyAccountsFrameContainer iframe'); if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`); @@ -60,19 +60,24 @@ try { await iframe.locator('#login_username').fill(email); await iframe.locator('#login_password').fill(password); await iframe.locator('#login_login').click(); - // handle MFA, but don't await it - iframe.locator('form[name=second_step_authentication]').waitFor().then(async () => { - console.log('Two-Step Verification - Enter security code'); - console.log(await iframe.locator('.form__description').innerText()); - const otp = await prompt({ type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 4 || 'The code must be 4 digits!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), { delay: 10 }); - await iframe.locator('#second_step_authentication_send').click(); - await page.waitForTimeout(1000); // TODO still needed with wait for username below? - }).catch(_ => { }); - iframe.locator('text=Invalid captcha').waitFor().then(() => { - 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.'); - notify('gog: got captcha during login. Please check.'); - }).catch(_ => { }); + void (async () => { + try { + await iframe.locator('form[name=second_step_authentication]').waitFor({ timeout: 15000 }); + console.log('Two-Step Verification - Enter security code'); + console.log(await iframe.locator('.form__description').innerText()); + const otp = await prompt({ type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 4 || 'The code must be 4 digits!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them + await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), { delay: 10 }); + await iframe.locator('#second_step_authentication_send').click(); + await page.waitForTimeout(1000); + } catch {} + })(); + void (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.'); + notify('gog: got captcha during login. Please check.'); + } catch {} + })(); await page.waitForSelector('#menuUsername'); } else { console.log('Waiting for you to login in the browser.'); @@ -101,11 +106,9 @@ try { console.log(`Current free game: ${chalk.blue(title)} - ${url}`); db.data[user][title] ||= { title, time: datetime(), url }; if (cfg.dryrun) process.exit(1); - // await page.locator('#giveaway:not(.is-loading)').waitFor(); // otherwise screenshot is sometimes with loading indicator instead of game title; #TODO fix, skipped due to timeout, see #240 await banner.screenshot({ path: screenshot(`${filenamify(title)}.png`) }); // overwrites every time - only keep first? - // await banner.getByRole('button', { name: 'Add to library' }).click(); - // instead of clicking the button, we visit the auto-claim URL which gives as a JSON response which is easier than checking the state of a button + // instead of clicking the button, visit the auto-claim URL which gives a JSON response await page.goto('https://www.gog.com/giveaway/claim'); const response = await page.innerText('body'); // console.log(response); diff --git a/prime-gaming.js b/prime-gaming.js index 00e8466..c881f79 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -6,7 +6,6 @@ import { cfg } from './src/config.js'; const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a); -// const URL_LOGIN = 'https://www.amazon.de/ap/signin'; // wrong. needs some session args to be valid? const URL_CLAIM = 'https://luna.amazon.com/claims/home'; console.log(datetime(), 'started checking prime-gaming'); @@ -25,14 +24,12 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { handleSIGINT(context); -// TODO test if needed await stealth(context); 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 }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it -// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); +await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it const notify_games = []; let user; @@ -62,14 +59,18 @@ try { await page.fill('[name=password]', password); await page.click('input[type="submit"]'); await handleMFA(page).catch(() => {}); - page.waitForURL('**/ap/signin**').then(async () => { // check for wrong credentials + try { + await page.waitForURL('**/ap/signin**'); const error = await page.locator('.a-alert-content').first().innerText(); - if (!error.trim.length) return; - console.error('Login error:', error); - await notify(`prime-gaming: login: ${error}`); - await context.close(); // finishes potential recording - process.exit(1); - }); + if (error.trim().length) { + console.error('Login error:', error); + await notify(`prime-gaming: login: ${error}`); + await context.close(); // finishes potential recording + process.exit(1); + } + } catch { + // if navigation succeeded, continue + } await page.waitForURL(/luna\.amazon\.com\/claims\/.*signedIn=true/); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); return true; @@ -91,7 +92,7 @@ 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, TODO does not work reliably, need to wait for something else first? + 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 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")'); @@ -105,16 +106,19 @@ try { await page.fill('[name=email]', email); await page.click('input[type="submit"]'); await page.fill('[name=password]', password); - // await page.check('[name=rememberMe]'); // no longer exists await page.click('input[type="submit"]'); - page.waitForURL('**/ap/signin**').then(async () => { // check for wrong credentials + try { + await page.waitForURL('**/ap/signin**'); const error = await page.locator('.a-alert-content').first().innerText(); - if (!error.trim.length) return; - console.error('Login error:', error); - await notify(`prime-gaming: login: ${error}`); - await context.close(); // finishes potential recording - process.exit(1); - }); + if (error.trim().length) { + console.error('Login error:', error); + await notify(`prime-gaming: login: ${error}`); + await context.close(); // finishes potential recording + process.exit(1); + } + } catch { + // navigation ok + } handleMFA(page).catch(() => {}); } else { console.log('Waiting for you to login in the browser.'); @@ -130,9 +134,6 @@ try { } user = await page.locator('[data-a-target="user-dropdown-first-name-text"], [data-testid="user-dropdown-first-name-text"]').first().innerText(); console.log(`Signed in as ${user}`); - // await page.click('button[aria-label="User dropdown and more options"]'); - // const twitch = await page.locator('[data-a-target="TwitchDisplayName"]').first().innerText(); - // console.log(`Twitch user name is ${twitch}`); db.data[user] ||= {}; if (await page.getByRole('button', { name: 'Try Prime' }).count()) { @@ -156,7 +157,7 @@ try { // loading all games became flaky; see https://github.com/vogler/free-games-claimer/issues/357 await page.keyboard.press('PageDown'); // scrolling to straight to the bottom started to skip loading some games await page.waitForLoadState('networkidle'); // wait for all games to be loaded - await page.waitForTimeout(3000); // TODO networkidle wasn't enough to load all already collected games + await page.waitForTimeout(3000); // extra wait to load all already collected games // do it again since once wasn't enough... await page.keyboard.press('PageDown'); await page.waitForTimeout(3000); @@ -372,9 +373,9 @@ try { for (const { title, url } of external_info) { console.log('Current free game:', chalk.blue(title)); // , url); - const existing = db.data[user]?.[title]; - if (existing && existing.status && !existing.status.startsWith('failed')) { - console.log(` Already recorded as ${existing.status}, skipping.`); + const existingStatus = db.data[user]?.[title]?.status; + if (existingStatus && !existingStatus.startsWith('failed')) { + console.log(` Already recorded as ${existingStatus}, skipping.`); notify_games.push({ title, url, status: 'existed' }); continue; } @@ -448,21 +449,21 @@ try { page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(() => {}), ]).catch(() => {}); db.data[user][title] ||= { title, time: datetime(), url, store }; - if (await page.locator('div:has-text("Link game account")').count() // TODO still needed? epic games store just has 'Link account' as the button text now. + if (await page.locator('div:has-text("Link game account")').count() // epic games store also shows "Link account" || await page.locator('div:has-text("Link account")').count()) { console.error(' Account linking is required to claim this offer!'); notify_game.status = `failed: need account linking for ${store}`; db.data[user][title].status = 'failed: need account linking'; // await page.pause(); // await page.click('[data-a-target="LinkAccountModal"] [data-a-target="LinkAccountButton"]'); - // TODO login for epic games also needed if already logged in + // login for epic games also needed if already logged in // wait for https://www.epicgames.com/id/authorize?redirect_uri=https%3A%2F%2Fservice.link.amazon.gg... // await page.click('button[aria-label="Allow"]'); } else { db.data[user][title].status = 'claimed'; // print code if there is one const redeem = { - // 'origin': 'https://www.origin.com/redeem', // TODO still needed or now only via account linking? + // 'origin': 'https://www.origin.com/redeem', // kept for legacy flows; current path uses account linking 'gog.com': 'https://www.gog.com/redeem', 'microsoft store': 'https://account.microsoft.com/billing/redeem', xbox: 'https://account.microsoft.com/billing/redeem', @@ -519,7 +520,7 @@ try { } else if (reason == 'code_not_found') { redeem_action = 'redeem (not found)'; console.error(' Code was not found!'); - } else { // TODO not logged in? need valid unused code to test. + } 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}`); @@ -569,12 +570,12 @@ try { if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') { redeem_action = 'already redeemed'; console.error(' error: UserAlreadyOwnsContent'); - } else { // TODO what's returned on success? + } else { // success path not seen yet; log below if needed redeem_action = 'redeemed'; db.data[user][title].status = 'claimed and redeemed?'; console.log(' Redeemed successfully? Please report if not in https://github.com/vogler/free-games-claimer/issues/5'); } - } else { // TODO find out other responses + } else { // other responses; keep info log for analysis redeem_action = 'unknown'; console.debug(` Response: ${rt}`); console.log(' Redeemed successfully? Please report your Response from above (if it is new) in https://github.com/vogler/free-games-claimer/issues/5'); @@ -672,7 +673,7 @@ try { const match = unlinked_store.match(/Link (.*) account/); if (match && match.length == 2) unlinked_store = match[1]; } 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()); // TODO needed? + 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'; } if (unlinked_store) { diff --git a/src/migrate.js b/src/migrate.js index 80abc9d..e28e88d 100644 --- a/src/migrate.js +++ b/src/migrate.js @@ -18,7 +18,6 @@ const datetime_UTCtoLocalTimezone = async file => { db.data[user][game].time = time2; } } - // console.log(db.data); await db.write(); // write out json db }; diff --git a/test/sigint-enquirer-raw.js b/test/sigint-enquirer-raw.js index c85ee0d..baf1e1c 100644 --- a/test/sigint-enquirer-raw.js +++ b/test/sigint-enquirer-raw.js @@ -1,36 +1,13 @@ // https://github.com/enquirer/enquirer/issues/372 import { prompt, handleSIGINT } from '../src/util.js'; -// const handleSIGINT = () => process.on('SIGINT', () => { // e.g. when killed by Ctrl-C -// console.log('\nInterrupted by SIGINT. Exit!'); -// process.exitCode = 130; -// }); handleSIGINT(); -// function onRawSIGINT(fn) { -// const { stdin, stdout } = process; -// stdin.setRawMode(true); -// stdin.resume(); -// stdin.on('data', data => { -// const key = data.toString('utf-8'); -// if (key === '\u0003') { // ctrl + c -// fn(); -// } else { -// stdout.write(key); -// } -// }); -// } -// onRawSIGINT(() => { -// console.log('raw'); process.exit(1); -// }); - console.log('hello'); console.error('hello error'); try { - let i = 'foo'; + let i = await prompt(); // SIGINT no longer handled if this is executed i = await prompt(); // SIGINT no longer handled if this is executed - i = await prompt(); // SIGINT no longer handled if this is executed - // handleSIGINT(); console.log('value:', i); setTimeout(() => console.log('timeout 3s'), 3000); } catch (e) { diff --git a/unrealengine.js b/unrealengine.js index 4f4de4b..8fe84b8 100644 --- a/unrealengine.js +++ b/unrealengine.js @@ -1,6 +1,3 @@ -// TODO This is mostly a copy of epic-games.js -// New assets to claim every first Tuesday of a month. - import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra import { authenticator } from 'otplib'; import path from 'node:path'; @@ -21,8 +18,7 @@ const db = await jsonDb('unrealengine.json', {}); const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, viewport: { width: cfg.width, height: cfg.height }, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? - // userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0 + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // Windows UA avoids "device not supported"; update when browser version changes locale: 'en-US', // ignore OS locale to be sure to have english text for locators recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 recordHar: cfg.record ? { path: `data/record/ue-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools @@ -36,8 +32,7 @@ await stealth(context); 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 }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it -// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); +await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it const notify_games = []; let user; @@ -60,23 +55,26 @@ try { const email = cfg.eg_email || await prompt({ message: 'Enter email' }); const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' })); if (email && password) { - // await page.click('text=Sign in with Epic Games'); await page.fill('#email', email); await page.click('button[type="submit"]'); await page.fill('#password', password); await page.click('button[type="submit"]'); - page.waitForSelector('#h_captcha_challenge_login_prod iframe').then(() => { - 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.'); - notify('unrealengine: got captcha during login. Please check.'); - }).catch(_ => { }); - // handle MFA, but don't await it - page.waitForURL('**/id/login/mfa**').then(async () => { - 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 ...'); - // TODO locator for text (email or app?) - 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!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); - await page.click('button[type="submit"]'); - }).catch(_ => { }); + void (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.'); + notify('unrealengine: got captcha during login. Please check.'); + } catch {} + })(); + void (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 ...'); + 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!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them + await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); + await page.click('button[type="submit"]'); + } catch {} + })(); } 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.'); @@ -135,18 +133,19 @@ try { notify('unrealengine: ' + err); process.exit(1); } - // await page.pause(); console.log('Click shopping cart'); await page.locator('.shopping-cart').click(); - // await page.waitForTimeout(2000); await page.locator('button.checkout').click(); console.log('Click checkout'); // maybe: Accept End User License Agreement - page.locator('[name=accept-label]').check().then(() => { - console.log('Accept End User License Agreement'); - page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies' - }).catch(_ => { }); - await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? + void (async () => { + try { + await page.locator('[name=accept-label]').check({ timeout: 10000 }); + console.log('Accept End User License Agreement'); + await page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies' + } catch {} + })(); + await page.waitForSelector('#webPurchaseContainer iframe'); const iframe = page.frameLocator('#webPurchaseContainer iframe'); if (cfg.debug) await page.pause(); @@ -161,14 +160,21 @@ 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")'); - btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' + void (async () => { + try { + await btnAgree.waitFor({ timeout: 10000 }); + await 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'); - 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.'); - }).catch(_ => { }); // may time out if not shown + void (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 {} + })(); // may time out if not shown await page.waitForSelector('text=Thank you'); for (const id of ids) { db.data[user][id].status = 'claimed'; @@ -176,16 +182,12 @@ try { } notify_games.forEach(g => g.status == 'failed' && (g.status = 'claimed')); console.log('Claimed successfully!'); - // context.setDefaultTimeout(cfg.timeout); } catch (e) { console.log(e); - // console.error(' Failed to claim! Try again if NopeCHA timed out. Click the extension to see if you ran out of credits (refill after 24h). To avoid captchas try to get a new IP or set a cookie from https://www.hcaptcha.com/accessibility'); console.error(' Failed to claim! To avoid captchas try to get a new IP address.'); await page.screenshot({ path: screenshot('failed', `${filenamify(datetime())}.png`), fullPage: true }); - // db.data[user][id].status = 'failed'; notify_games.forEach(g => g.status = 'failed'); } - // notify_game.status = db.data[user][game_id].status; // claimed or failed if (notify_games.length) await page.screenshot({ path: screenshot(`${filenamify(datetime())}.png`), fullPage: false }); // fullPage is quite long... console.log('Done');