From 747a2a5621543ea1f16059b86b0615c9900773cf Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 13 Jan 2022 20:04:13 +0100 Subject: [PATCH] claimer for Amazon Prime Gaming (no ext stores) --- main.stealth.js | 28 ++++++---------- prime-gaming.js | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ util.js | 14 ++++++++ 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 prime-gaming.js create mode 100644 util.js diff --git a/main.stealth.js b/main.stealth.js index 1e67677..2196e40 100644 --- a/main.stealth.js +++ b/main.stealth.js @@ -6,20 +6,6 @@ const URL_LOGIN = 'https://www.epicgames.com/login'; const URL_CLAIM = 'https://www.epicgames.com/store/en-US/free-games'; const TIMEOUT = 20 * 1000; // 20s, default is 30s -// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212 -const newStealthContext = async (browser, contextOptions = {}) => { - if (!debug) { // fix userAgent in headless mode - const dummyContext = await browser.newContext(); - const originalUserAgent = await (await dummyContext.newPage()).evaluate(() => navigator.userAgent); - await dummyContext.close(); - // console.log('originalUserAgent:', originalUserAgent); // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36 - contextOptions = { - ...contextOptions, - userAgent: originalUserAgent.replace("Headless", ""), // HeadlessChrome -> Chrome, TODO needed? - }; - } -}; - // could change to .mjs to get top-level-await, but would then also need to change require to import and dynamic import for stealth below would just add more async/await (async () => { // https://playwright.dev/docs/auth#multi-factor-authentication @@ -27,7 +13,7 @@ const newStealthContext = async (browser, contextOptions = {}) => { channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge headless: false, viewport: { width: 1280, height: 1280 }, - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36', // see replace of Headless in newStealthContext above. TODO update if browser is updated! + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated! }); // Without stealth plugin, the website shows an hcaptcha on login with username/password and in the last step of claiming a game. It may have other heuristics like unsuccessful logins as well. After <6h (TBD) it resets to no captcha again. Getting a new IP also resets. @@ -66,11 +52,17 @@ const newStealthContext = async (browser, contextOptions = {}) => { // end stealth setup if (!debug) context.setDefaultTimeout(TIMEOUT); - const page = context.pages.length ? context.pages[0] : await context.newPage(); + const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist console.log('userAgent:', await page.evaluate(() => navigator.userAgent)); + + const clickIfExists = async selector => { + if (await page.locator(selector).count() > 0) + await page.click(selector); + }; + await page.goto(URL_CLAIM, {waitUntil: 'domcontentloaded'}); // default 'load' takes forever - // with persistent context the cookie message will only show up the first time, so we can't unconditionally wait for it - just let the user click it. - // await page.click('button:has-text("Accept All Cookies")'); // to not waste screen space in --debug + // with persistent context the cookie message will only show up the first time, so we can't unconditionally wait for it - try to catch it or let the user click it. + await clickIfExists('button:has-text("Accept All Cookies")'); // to not waste screen space in --debug while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page."); context.setDefaultTimeout(0); // give user time to log in without timeout diff --git a/prime-gaming.js b/prime-gaming.js new file mode 100644 index 0000000..ec3e5cb --- /dev/null +++ b/prime-gaming.js @@ -0,0 +1,89 @@ +//@ts-check +const { chromium } = require('playwright'); // stealth plugin needs no outdated playwright-extra +const debug = process.env.PWDEBUG == '1'; // runs headful and opens https://playwright.dev/docs/inspector + +// const URL_LOGIN = 'https://www.amazon.de/ap/signin'; // wrong. needs some session args to be valid? +const URL_CLAIM = 'https://gaming.amazon.com/home'; +const TIMEOUT = 20 * 1000; // 20s, default is 30s + +// could change to .mjs to get top-level-await, but would then also need to change require to import and dynamic import for stealth below would just add more async/await +(async () => { + // https://playwright.dev/docs/auth#multi-factor-authentication + const context = await chromium.launchPersistentContext('userDataDir', { + channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge + headless: false, + viewport: { width: 1280, height: 1280 }, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36', // see replace of Headless in newStealthContext above. TODO update if browser is updated! + }); + + // stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212 + // https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions + const enabledEvasions = [ + 'chrome.app', + 'chrome.csi', + 'chrome.loadTimes', + 'chrome.runtime', + // 'defaultArgs', + 'iframe.contentWindow', + 'media.codecs', + 'navigator.hardwareConcurrency', + 'navigator.languages', + 'navigator.permissions', + 'navigator.plugins', + // 'navigator.vendor', + 'navigator.webdriver', + 'sourceurl', + // 'user-agent-override', // doesn't work since playwright has no page.browser() + 'webgl.vendor', + 'window.outerdimensions' + ]; + const evasions = enabledEvasions.map(e => new require(`puppeteer-extra-plugin-stealth/evasions/${e}`)); + const stealth = { + callbacks: [], + async evaluateOnNewDocument(...args) { + this.callbacks.push({ cb: args[0], a: args[1] }) + } + } + evasions.forEach(e => e().onPageCreated(stealth)); + for (let evasion of stealth.callbacks) { + await context.addInitScript(evasion.cb, evasion.a); + } + // end stealth setup + + if (!debug) context.setDefaultTimeout(TIMEOUT); + // const page = /* context.pages().length ? context.pages[0] : */ await context.newPage(); + const page = context.pages()[0]; + console.log('userAgent:', await page.evaluate(() => navigator.userAgent)); + + const clickIfExists = async selector => { + if (await page.locator(selector).count() > 0) + await page.click(selector); + }; + + await page.goto(URL_CLAIM, {waitUntil: 'domcontentloaded'}); // default 'load' takes forever + await clickIfExists('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space in --debug + while (await page.locator('button:has-text("Sign in")').count() > 0) { + console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page."); + await page.click('button:has-text("Sign in")'); + if (!debug) context.setDefaultTimeout(0); // give user time to log in without timeout + await page.waitForNavigation({url: 'https://gaming.amazon.com/home?signedIn=true'}); + if (!debug) context.setDefaultTimeout(TIMEOUT); + } + console.log('Signed in.'); + const game_sel = 'div[data-a-target="offer-list-FGWP_FULL"] .offer__action:has-text("Claim game")'; + await page.waitForSelector(game_sel); // TODO this will just time out if all games are already claimed! + const n = await page.locator(game_sel).count(); + console.log('Number of free unclaimed games:', n); + console.log('Number of already claimed games:', await page.locator('div[data-a-target="offer-list-FGWP_FULL"] p:has-text("Claimed")').count()); + const games = await page.$$(game_sel); + // for (let i=1; i<=n; i++) { + for (const card of games) { + // const card = page.locator(`:nth-match(${game_sel}, ${i})`); // this will reevaluate after games are claimed and index will be wrong + // const title = await card.locator('h3').first().innerText(); + const title = await (await card.$('h3')).innerText(); + console.log('Current free game:', title); + await (await card.$('button')).click(); + // await page.pause(); + } + await context.close(); +})(); diff --git a/util.js b/util.js new file mode 100644 index 0000000..5f2e302 --- /dev/null +++ b/util.js @@ -0,0 +1,14 @@ + +// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212 +const newStealthContext = async (browser, contextOptions = {}) => { + if (!debug) { // fix userAgent in headless mode + const dummyContext = await browser.newContext(); + const originalUserAgent = await (await dummyContext.newPage()).evaluate(() => navigator.userAgent); + await dummyContext.close(); + // console.log('originalUserAgent:', originalUserAgent); // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36 + contextOptions = { + ...contextOptions, + userAgent: originalUserAgent.replace("Headless", ""), // HeadlessChrome -> Chrome, TODO needed? + }; + } +};