diff --git a/config.js b/config.js new file mode 100644 index 0000000..51e4dcb --- /dev/null +++ b/config.js @@ -0,0 +1,19 @@ +// import * as dotenv from 'dotenv'; +// dotenv.config({ path: 'data/config.env' }); + +export const cfg = { + debug: process.env.PWDEBUG == '1', // runs non-headless and opens https://playwright.dev/docs/inspector + dryrun: process.env.DRYRUN == '1', // don't claim anything + show: process.env.SHOW == '1', // run non-headless + get headless() { return !this.debug && !this.show }, + width: Number(process.env.WIDTH) || 1280, + height: Number(process.env.HEIGHT) || 1280, + timeout: (Number(process.env.TIMEOUT) || 20) * 1000, // 20s, default for playwright is 30s + novnc_port: process.env.NOVNC_PORT, + eg_email: process.env.EG_EMAIL || process.env.EMAIL, + eg_password: process.env.EG_PASSWORD || process.env.PASSWORD, + pg_email: process.env.PG_EMAIL || process.env.EMAIL, + pg_password: process.env.PG_PASSWORD || process.env.PASSWORD, + gog_email: process.env.GOG_EMAIL || process.env.EMAIL, + gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD, +}; diff --git a/epic-games.js b/epic-games.js index 675bb89..fb1a604 100644 --- a/epic-games.js +++ b/epic-games.js @@ -1,6 +1,7 @@ import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra import path from 'path'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; +import { cfg } from './config.js'; import { existsSync, writeFileSync } from 'fs'; import prompts from 'prompts'; // alternatives: enquirer, inquirer @@ -8,15 +9,8 @@ import prompts from 'prompts'; // alternatives: enquirer, inquirer // single prompt that just returns the non-empty value instead of an object - why name things if there's just one? const prompt = async o => (await prompts({name: 'name', type: 'text', message: 'Enter value', validate: s => s.length, ...o})).name; -const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector -const show = process.env.SHOW == '1'; -const headless = !debug && !show; - 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 TIMEOUT = 20 * 1000; // 20s, default is 30s -const WIDTH = Number(process.env.WIDTH) || 1280; -const HEIGHT = Number(process.env.HEIGHT) || 1280; console.log(datetime(), 'started checking epic-games'); @@ -40,8 +34,8 @@ const ext = path.resolve('nopecha'); // used in Chromium, currently not needed i const context = await firefox.launchPersistentContext(dirs.browser, { // chrome will not work in linux arm64, only chromium // channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge - headless, - viewport: { width: WIDTH, height: HEIGHT }, + 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 locale: "en-US", // ignore OS locale to be sure to have english text for locators @@ -59,7 +53,7 @@ const context = await firefox.launchPersistentContext(dirs.browser, { // 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. await stealth(context); -if (!debug) context.setDefaultTimeout(TIMEOUT); +if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist // console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); @@ -73,13 +67,13 @@ try { while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { console.error('Not signed in anymore. Please login in the browser or here in the terminal.'); - if (process.env.NOVNC_PORT) console.info(`Open http://localhost:${process.env.NOVNC_PORT} to login inside the docker container.`); + if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`); context.setDefaultTimeout(0); // give user time to log in without timeout await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' }); console.info('Press ESC to skip if you want to login in the browser.'); - const email = process.env.EG_EMAIL || process.env.EMAIL || await prompt({message: 'Enter email'}); - const password = process.env.EG_PASSWORD || process.env.PASSWORD || await prompt({type: 'password', message: 'Enter password'}); + const email = cfg.eg_email || await prompt({message: 'Enter email'}); + const password = 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); @@ -99,7 +93,7 @@ try { console.log('Waiting for you to login in the browser.'); } await page.waitForNavigation({ url: URL_CLAIM }); - context.setDefaultTimeout(TIMEOUT); + context.setDefaultTimeout(cfg.timeout); } const user = await page.locator('#user span').first().innerHTML(); console.log(`Signed in as ${user}`); @@ -143,8 +137,8 @@ try { // 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? - if (process.env.DRYRUN) continue; - if (debug) await page.pause(); + if (cfg.dryrun) continue; + if (cfg.debug) await page.pause(); // it then creates an iframe for the purchase await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? @@ -170,7 +164,7 @@ try { db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed time console.log(' Claimed successfully!'); - context.setDefaultTimeout(TIMEOUT); + 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'); diff --git a/gog.js b/gog.js index 75afc9c..4e2e0bf 100644 --- a/gog.js +++ b/gog.js @@ -1,20 +1,14 @@ import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra import path from 'path'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; +import { cfg } from './config.js'; import prompts from 'prompts'; // alternatives: enquirer, inquirer // import enquirer from 'enquirer'; const { prompt } = enquirer; // single prompt that just returns the non-empty value instead of an object - why name things if there's just one? const prompt = async o => (await prompts({name: 'name', type: 'text', message: 'Enter value', validate: s => s.length, ...o})).name; -const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector -const show = process.argv.includes('show', 2); -const headless = !debug && !show; - const URL_CLAIM = 'https://www.gog.com/en'; -const TIMEOUT = 20 * 1000; // 20s, default is 30s -const WIDTH = Number(process.env.WIDTH) || 1280; -const HEIGHT = Number(process.env.HEIGHT) || 1280; console.log(datetime(), 'started checking gog'); @@ -23,12 +17,12 @@ db.data ||= {}; // https://playwright.dev/docs/auth#multi-factor-authentication const context = await firefox.launchPersistentContext(dirs.browser, { - headless, - viewport: { width: WIDTH, height: HEIGHT }, + headless: cfg.headless, + viewport: { width: cfg.width, height: cfg.height }, locale: "en-US", // ignore OS locale to be sure to have english text for locators -> done via /en in URL }); -if (!debug) context.setDefaultTimeout(TIMEOUT); +if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist // console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); @@ -46,10 +40,10 @@ try { // it then creates an iframe for the login await page.waitForSelector('#GalaxyAccountsFrameContainer iframe'); // TODO needed? const iframe = page.frameLocator('#GalaxyAccountsFrameContainer iframe'); - if (!debug) context.setDefaultTimeout(0); // give user time to log in without timeout + if (!cfg.debug) context.setDefaultTimeout(0); // give user time to log in without timeout console.info('Press ESC to skip if you want to login in the browser (not possible in headless mode).'); - const email = process.env.GOG_EMAIL || process.env.EMAIL || await prompt({message: 'Enter email'}); - const password = process.env.GOG_PASSWORD || process.env.PASSWORD || await prompt({type: 'password', message: 'Enter password'}); + const email = cfg.gog_email || await prompt({message: 'Enter email'}); + const password = cfg.gog_password || await prompt({type: 'password', message: 'Enter password'}); if (email && password) { iframe.locator('a[href="/logout"]').click().catch(_ => { }); // Click 'Change account' (email from previous login is set in some cookie) await iframe.locator('#login_username').fill(email); @@ -65,7 +59,7 @@ try { await page.waitForTimeout(1000); // TODO wait for something else below? }); } else { - if (headless) { + if (cfg.headless) { console.log('Please run `node gog show` to login in the opened browser.'); await context.close(); // not needed? process.exit(1); @@ -73,7 +67,7 @@ try { console.log('Waiting for you to login in the browser.'); } // await page.waitForNavigation(); // TODO was blocking - if (!debug) context.setDefaultTimeout(TIMEOUT); + if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); } const user = await page.locator('#menuUsername').first().innerHTML(); console.log(`Signed in as ${user}`); diff --git a/prime-gaming.js b/prime-gaming.js index 26c2eaa..940cc9e 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -1,21 +1,15 @@ import { firefox } from 'playwright'; // stealth plugin needs no outdated playwright-extra import path from 'path'; import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js'; +import { cfg } from './config.js'; import prompts from 'prompts'; // alternatives: enquirer, inquirer // import enquirer from 'enquirer'; const { prompt } = enquirer; // single prompt that just returns the non-empty value instead of an object - why name things if there's just one? const prompt = async o => (await prompts({name: 'name', type: 'text', message: 'Enter value', validate: s => s.length, ...o})).name; -const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector -const show = process.env.SHOW == '1'; -const headless = !debug && !show; - // 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 -const WIDTH = Number(process.env.WIDTH) || 1280; -const HEIGHT = Number(process.env.HEIGHT) || 1280; console.log(datetime(), 'started checking prime-gaming'); @@ -33,15 +27,15 @@ const migrateDb = (user) => { // https://playwright.dev/docs/auth#multi-factor-authentication const context = await firefox.launchPersistentContext(dirs.browser, { - headless, - viewport: { width: WIDTH, height: HEIGHT }, + headless: cfg.headless, + viewport: { width: cfg.width, height: cfg.height }, locale: "en-US", // ignore OS locale to be sure to have english text for locators }); // TODO test if needed await stealth(context); -if (!debug) context.setDefaultTimeout(TIMEOUT); +if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist // console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); @@ -54,10 +48,10 @@ try { while (await page.locator('button:has-text("Sign in")').count() > 0) { console.error('Not signed in anymore.'); await page.click('button:has-text("Sign in")'); - if (!debug) context.setDefaultTimeout(0); // give user time to log in without timeout + if (!cfg.debug) context.setDefaultTimeout(0); // give user time to log in without timeout console.info('Press ESC to skip if you want to login in the browser (not possible in default headless mode).'); - const email = process.env.PG_EMAIL || process.env.EMAIL || await prompt({message: 'Enter email'}); - const password = process.env.PG_PASSWORD || process.env.PASSWORD || await prompt({type: 'password', message: 'Enter password'}); + const email = cfg.pg_email || await prompt({message: 'Enter email'}); + const password = cfg.pg_password || await prompt({type: 'password', message: 'Enter password'}); if (email && password) { await page.fill('[name=email]', email); await page.fill('[name=password]', password); @@ -72,7 +66,7 @@ try { await page.click('input[type="submit"]'); }); } else { - if (headless) { + if (cfg.headless) { console.log('Please run `node prime-gaming show` to login in the opened browser.'); await context.close(); // not needed? process.exit(1); @@ -80,7 +74,7 @@ try { console.log('Waiting for you to login in the browser.'); } await page.waitForNavigation({ url: 'https://gaming.amazon.com/home?signedIn=true' }); - if (!debug) context.setDefaultTimeout(TIMEOUT); + if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); } const user = await page.locator('[data-a-target="user-dropdown-first-name-text"]').first().innerText(); console.log(`Signed in as ${user}`); @@ -103,7 +97,7 @@ try { // const title = await card.locator('h3').first().innerText(); const title = await (await card.$('.item-card-details__body__primary')).innerText(); console.log('Current free game:', title); - if (process.env.DRYRUN) continue; + if (cfg.dryrun) continue; // const img = await (await card.$('img.tw-image')).getAttribute('src'); // console.log('Image:', img); const p = path.resolve(dirs.screenshots, 'prime-gaming', 'internal', `${filenamify(title)}.png`); @@ -122,7 +116,7 @@ try { if (!card) break; const title = await (await card.$('.item-card-details__body__primary')).innerText(); console.log('Current free game:', title); - if (process.env.DRYRUN) continue; + if (cfg.dryrun) continue; await (await card.$('text=Claim')).click(); // await page.waitForNavigation(); await Promise.any([page.click('button:has-text("Claim now")'), page.click('button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")')]); // waits for navigation