diff --git a/README.md b/README.md
index 265bfad..43f9e53 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 bd41c7d..877463f 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 b2406cc..0000000
--- 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...
- }
-}