From 748bf41f467efe2d02785281f8a8ad828d1508e4 Mon Sep 17 00:00:00 2001 From: Jonatino Date: Thu, 4 Jan 2024 23:08:03 -0500 Subject: [PATCH] remove xbox plugin, games with gold doesn't exist anymore --- README.md | 6 -- src/config.js | 4 - xbox.js | 251 -------------------------------------------------- 3 files changed, 261 deletions(-) delete mode 100644 xbox.js diff --git a/README.md b/README.md index 265bfad4..43f9e538 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Claims free games periodically on - [Amazon Prime Gaming](https://gaming.amazon.com) - [GOG](https://www.gog.com) - [Unreal Engine (Assets)](https://www.unrealengine.com/marketplace/en-US/assets?count=20&sortBy=effectiveDate&sortDir=DESC&start=0&tag=4910) ([experimental](https://github.com/vogler/free-games-claimer/issues/44), same login as Epic Games) - Pull requests welcome :) @@ -119,7 +118,6 @@ To get the OTP key, it is easiest to follow the store's guide for adding an auth - **Epic Games**: visit [password & security](https://www.epicgames.com/account/password), enable 'third-party authenticator app', copy the 'Manual Entry Key' and use it to set `EG_OTPKEY`. - **Prime Gaming**: visit Amazon 'Your Account › Login & security', 2-step verification › Manage › Add new app › Can't scan the barcode, copy the bold key and use it to set `PG_OTPKEY` - **GOG**: only offers OTP via email - Beware that storing passwords and OTP keys as clear text may be a security risk. Use a unique/generated password! TODO: maybe at least offer to base64 encode for storage. @@ -137,16 +135,12 @@ Claiming the Amazon Games works out-of-the-box, however, for games on external s Keys and URLs are printed to the console, included in notifications and saved in `data/prime-gaming.json`. A screenshot of the page with the key is also saved to `data/screenshots`. [TODO](https://github.com/vogler/free-games-claimer/issues/5): ~~redeem keys on external stores.~~ - - - ### Run periodically #### How often? Epic Games usually has two free games *every week*, before Christmas every day. Prime Gaming has new games *every month* or more often during Prime days. GOG usually has one new game every couples of weeks. Unreal Engine has new assets to claim *every first Tuesday of a month*. - It is safe to run the scripts every day. diff --git a/src/config.js b/src/config.js index bd41c7d1..877463f3 100644 --- a/src/config.js +++ b/src/config.js @@ -42,10 +42,6 @@ export const cfg = { gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD, gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game // OTP only via GOG_EMAIL, can't add app... - // auth xbox - xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL, - xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD, - xbox_otpkey: process.env.XBOX_OTPKEY, // experimmental - likely to change pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content diff --git a/xbox.js b/xbox.js deleted file mode 100644 index b2406cce..00000000 --- a/xbox.js +++ /dev/null @@ -1,251 +0,0 @@ -import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra -import { authenticator } from 'otplib'; -import { - datetime, - handleSIGINT, - html_game_list, - jsonDb, - notify, - prompt, -} from './src/util.js'; -import { cfg } from './src/config.js'; - -// ### SETUP -const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold"; - -console.log(datetime(), 'started checking xbox'); - -const db = await jsonDb('xbox.json'); -db.data ||= {}; - -handleSIGINT(); - -// https://playwright.dev/docs/auth#multi-factor-authentication -const context = await firefox.launchPersistentContext(cfg.dir.browser, { - 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 (!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 - -const notify_games = []; -let user; - -main(); - -async function main() { - try { - await performLogin(); - await getAndSaveUser(); - await redeemFreeGames(); - } catch (error) { - console.error(error); - process.exitCode ||= 1; - if (error.message && process.exitCode != 130) notify(`xbox failed: ${error.message.split('\n')[0]}`); - } finally { - await db.write(); // write out json db - if (notify_games.filter(g => g.status != 'existed').length) { - // don't notify if all were already claimed - notify(`xbox (${user}):
${html_game_list(notify_games)}`); - } - await context.close(); - } -} - -async function performLogin() { - await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever - - const signInLocator = page - .getByRole('link', { - name: 'Sign in to your account', - }) - .first(); - const usernameLocator = page - .getByRole('button', { - name: 'Account manager for', - }) - .first(); - - await Promise.any([signInLocator.waitFor(), usernameLocator.waitFor()]); - - if (await usernameLocator.isVisible()) { - return; // logged in using saved cookie - } else if (await signInLocator.isVisible()) { - console.error('Not signed in anymore.'); - await signInLocator.click(); - await signInToXbox(); - } else { - console.error('lost! where am i?'); - } -} - -async function signInToXbox() { - page.waitForLoadState('domcontentloaded'); - 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!`); - - // ### FETCH EMAIL/PASS - if (cfg.xbox_email && cfg.xbox_password) console.info('Using email and password from environment.'); - else console.info( - 'Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).', - ); - const email = cfg.xbox_email || await prompt({ message: 'Enter email' }); - const password = - email && - (cfg.xbox_password || - await prompt({ - type: 'password', - message: 'Enter password', - })); - // ### FILL IN EMAIL/PASS - if (email && password) { - const usernameLocator = page - .getByPlaceholder('Email, phone, or Skype') - .first(); - const passwordLocator = page.getByPlaceholder('Password').first(); - - await Promise.any([ - usernameLocator.waitFor(), - passwordLocator.waitFor(), - ]); - - // username may already be saved from before, if so, skip to filling in password - if (await page.getByPlaceholder('Email, phone, or Skype').isVisible()) { - await usernameLocator.fill(email); - await page.getByRole('button', { name: 'Next' }).click(); - } - - await passwordLocator.fill(password); - await page.getByRole('button', { name: 'Sign in' }).click(); - - // handle MFA, but don't await it - page.locator('input[name="otc"]') - .waitFor() - .then(async () => { - console.log('Two-Step Verification - Enter security code'); - console.log( - await page - .locator('div[data-bind="text: description"]') - .innerText(), - ); - const otp = - cfg.xbox_otpkey && - authenticator.generate(cfg.xbox_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.type('input[name="otc"]', otp.toString()); - await page - .getByLabel('Don\'t ask me again on this device') - .check(); // Trust this Browser - await page.getByRole('button', { name: 'Verify' }).click(); - }) - .catch(_ => {}); - - // Trust this browser, but don't await it - page.getByLabel('Don\'t show this again') - .waitFor() - .then(async () => { - await page.getByLabel('Don\'t show this again').check(); - await page.getByRole('button', { name: 'Yes' }).click(); - }) - .catch(_ => {}); - } else { - console.log('Waiting for you to login in the browser.'); - await notify( - 'xbox: no longer signed in and not enough options set for automatic login.', - ); - if (cfg.headless) { - console.log( - 'Run `SHOW=1 node xbox` to login in the opened browser.', - ); - await context.close(); - process.exit(1); - } - } - - // ### VERIFY SIGNED IN - await page.waitForURL(`${URL_CLAIM}**`); - - if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); -} - -async function getAndSaveUser() { - user = await page.locator('#mectrl_currentAccount_primary').innerHTML(); - console.log(`Signed in as '${user}'`); - db.data[user] ||= {}; -} - -async function redeemFreeGames() { - const monthlyGamesLocator = await page.locator('.f-size-large').all(); - - const monthlyGamesPageLinks = await Promise.all( - monthlyGamesLocator.map( - async el => await el.locator('a').getAttribute('href'), - ), - ); - console.log('Free games:', monthlyGamesPageLinks); - - for (const url of monthlyGamesPageLinks) { - await page.goto(url); - - const title = await page.locator('h1').first().innerText(); - 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); - const notify_game = { title, url, status: 'failed' }; - notify_games.push(notify_game); // status is updated below - - // SELECTORS - const getBtnLocator = page.getByText('GET', { exact: true }).first(); - const installToLocator = page - .getByText('INSTALL TO', { exact: true }) - .first(); - - await Promise.any([ - getBtnLocator.waitFor(), - installToLocator.waitFor(), - ]); - - if (await installToLocator.isVisible()) { - console.log(' Already in library! Nothing to claim.'); - notify_game.status = 'existed'; - db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed - } else if (await getBtnLocator.isVisible()) { - console.log(' Not in library yet! Click GET.'); - await getBtnLocator.click(); - - // wait for popup - await page - .locator('iframe[name="purchase-sdk-hosted-iframe"]') - .waitFor(); - const popupLocator = page.frameLocator( - '[name=purchase-sdk-hosted-iframe]', - ); - - const finalGetBtnLocator = popupLocator.getByText('GET'); - await finalGetBtnLocator.waitFor(); - await finalGetBtnLocator.click(); - - await page.getByText('Thank you for your purchase.').waitFor(); - notify_game.status = 'claimed'; - db.data[user][game_id].status = 'claimed'; - db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time - console.log(' Claimed successfully!'); - } - - // notify_game.status = db.data[user][game_id].status; // claimed or failed - - // const p = path.resolve(cfg.dir.screenshots, playstation-plus', `${game_id}.png`); - // if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... - } -}