run eslint --fix .
This commit is contained in:
parent
011eddf97a
commit
0832ae57f5
10 changed files with 259 additions and 263 deletions
|
|
@ -11,7 +11,9 @@ export const cfg = {
|
|||
dryrun: process.env.DRYRUN == '1', // don't claim anything
|
||||
interactive: process.env.INTERACTIVE == '1', // confirm to claim, default skip
|
||||
show: process.env.SHOW == '1', // run non-headless
|
||||
get headless() { return !this.debug && !this.show },
|
||||
get headless() {
|
||||
return !this.debug && !this.show;
|
||||
},
|
||||
width: Number(process.env.WIDTH) || 1920, // width of the opened browser
|
||||
height: Number(process.env.HEIGHT) || 1080, // height of the opened browser
|
||||
timeout: (Number(process.env.TIMEOUT) || 60) * 1000, // default timeout for playwright is 30s
|
||||
|
|
@ -23,7 +25,7 @@ export const cfg = {
|
|||
return {
|
||||
browser: process.env.BROWSER_DIR || dataDir('browser'), // for multiple accounts or testing
|
||||
screenshots: process.env.SCREENSHOTS_DIR || dataDir('screenshots'), // set to 0 to disable screenshots
|
||||
}
|
||||
};
|
||||
},
|
||||
// auth epic-games
|
||||
eg_email: process.env.EG_EMAIL || process.env.EMAIL,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
|||
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 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
|
||||
locale: "en-US", // ignore OS locale to be sure to have english text for locators
|
||||
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-${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
|
||||
|
|
@ -64,7 +64,7 @@ const notify_games = [];
|
|||
let user;
|
||||
|
||||
try {
|
||||
await context.addCookies([{name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5*24*60*60*1000).toISOString(), domain: '.epicgames.com', path: '/'}]); // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
|
||||
await context.addCookies([{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' }]); // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
|
||||
|
||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // 'domcontentloaded' faster than default 'load' https://playwright.dev/docs/api/class-page#page-goto
|
||||
|
||||
|
|
@ -77,12 +77,12 @@ try {
|
|||
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.`);
|
||||
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!`);
|
||||
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
||||
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
||||
if (cfg.eg_email && cfg.eg_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.eg_email || await prompt({message: 'Enter email'});
|
||||
const password = email && (cfg.eg_password || await prompt({type: 'password', message: 'Enter password'}));
|
||||
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);
|
||||
|
|
@ -100,7 +100,7 @@ try {
|
|||
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
|
||||
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(_ => { });
|
||||
|
|
@ -134,7 +134,7 @@ try {
|
|||
// clicking on `game_sel` sometimes led to a 404, see https://github.com/vogler/free-games-claimer/issues/25
|
||||
// debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking.
|
||||
// Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions
|
||||
// filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
|
||||
// i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
|
||||
const urlSlugs = await Promise.all((await game_loc.elementHandles()).map(a => a.getAttribute('href')));
|
||||
const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
|
||||
console.log('Free games:', urls);
|
||||
|
|
@ -235,7 +235,7 @@ try {
|
|||
// 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
|
||||
await page.locator('text=Thanks for your order!').waitFor({state: 'attached'});
|
||||
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!');
|
||||
|
|
@ -260,8 +260,7 @@ try {
|
|||
process.exitCode ||= 1;
|
||||
console.error('--- Exception:');
|
||||
console.error(error); // .toString()?
|
||||
if (error.message && process.exitCode != 130)
|
||||
notify(`epic-games failed: ${error.message.split('\n')[0]}`);
|
||||
if (error.message && process.exitCode != 130) notify(`epic-games failed: ${error.message.split('\n')[0]}`);
|
||||
} finally {
|
||||
await db.write(); // write out json db
|
||||
if (notify_games.filter(g => g.status == 'claimed' || g.status == 'failed').length) { // don't notify if all have status 'existed', 'manual', 'requires base game', 'unavailable-in-region', 'skipped'
|
||||
|
|
|
|||
25
gog.js
25
gog.js
|
|
@ -14,7 +14,7 @@ const db = await jsonDb('gog.json', {});
|
|||
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
|
||||
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
|
||||
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/gog-${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
|
||||
|
|
@ -31,7 +31,7 @@ const notify_games = [];
|
|||
let user;
|
||||
|
||||
try {
|
||||
await context.addCookies([{name: 'CookieConsent', value: '{stamp:%274oR8MJL+bxVlG6g+kl2we5+suMJ+Tv7I4C5d4k+YY4vrnhCD+P23RQ==%27%2Cnecessary:true%2Cpreferences:true%2Cstatistics:true%2Cmarketing:true%2Cmethod:%27explicit%27%2Cver:1%2Cutc:1672331618201%2Cregion:%27de%27}', domain: 'www.gog.com', path: '/'}]); // to not waste screen space when non-headless
|
||||
await context.addCookies([{ name: 'CookieConsent', value: '{stamp:%274oR8MJL+bxVlG6g+kl2we5+suMJ+Tv7I4C5d4k+YY4vrnhCD+P23RQ==%27%2Cnecessary:true%2Cpreferences:true%2Cstatistics:true%2Cmarketing:true%2Cmethod:%27explicit%27%2Cver:1%2Cutc:1672331618201%2Cregion:%27de%27}', domain: 'www.gog.com', path: '/' }]); // to not waste screen space when non-headless
|
||||
|
||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||
|
||||
|
|
@ -45,11 +45,11 @@ try {
|
|||
await page.waitForSelector('#GalaxyAccountsFrameContainer iframe'); // TODO needed?
|
||||
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!`);
|
||||
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
||||
if (cfg.gog_email && cfg.gog_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.gog_email || await prompt({message: 'Enter email'});
|
||||
const password = email && (cfg.gog_password || await prompt({type: 'password', message: 'Enter password'}));
|
||||
const email = cfg.gog_email || await prompt({ message: 'Enter email' });
|
||||
const password = email && (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);
|
||||
|
|
@ -58,9 +58,9 @@ try {
|
|||
// 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});
|
||||
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(_ => { });
|
||||
|
|
@ -71,7 +71,7 @@ try {
|
|||
notify('gog: got captcha during login. Please check.');
|
||||
// TODO solve reCAPTCHA?
|
||||
}).catch(_ => { });
|
||||
await page.waitForSelector('#menuUsername')
|
||||
await page.waitForSelector('#menuUsername');
|
||||
} else {
|
||||
console.log('Waiting for you to login in the browser.');
|
||||
await notify('gog: no longer signed in and not enough options set for automatic login.');
|
||||
|
|
@ -129,7 +129,7 @@ try {
|
|||
notify_games.push({ title, url, status });
|
||||
|
||||
if (status == 'claimed' && !cfg.gog_newsletter) {
|
||||
console.log("Unsubscribe from 'Promotions and hot deals' newsletter");
|
||||
console.log('Unsubscribe from \'Promotions and hot deals\' newsletter');
|
||||
await page.goto('https://www.gog.com/en/account/settings/subscriptions');
|
||||
await page.locator('li:has-text("Marketing communications through Trusted Partners") label').uncheck();
|
||||
await page.locator('li:has-text("Promotions and hot deals") label').uncheck();
|
||||
|
|
@ -139,13 +139,12 @@ try {
|
|||
process.exitCode ||= 1;
|
||||
console.error('--- Exception:');
|
||||
console.error(error); // .toString()?
|
||||
if (error.message && process.exitCode != 130)
|
||||
notify(`gog failed: ${error.message.split('\n')[0]}`);
|
||||
if (error.message && process.exitCode != 130) notify(`gog 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(`gog (${user}):<br>${html_game_list(notify_games)}`);
|
||||
}
|
||||
}
|
||||
if (page.video()) console.log('Recorded video:', await page.video().path())
|
||||
if (page.video()) console.log('Recorded video:', await page.video().path());
|
||||
await context.close();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { JSONFile } from 'lowdb/node';
|
|||
import { datetime } from './util.js';
|
||||
|
||||
const datetime_UTCtoLocalTimezone = async file => {
|
||||
if (!existsSync(file))
|
||||
return console.error('File does not exist:', file);
|
||||
if (!existsSync(file)) return console.error('File does not exist:', file);
|
||||
const db = new Low(new JSONFile(file));
|
||||
await db.read();
|
||||
db.data ||= {};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-constant-condition */
|
||||
import { delay, html_game_list, notify } from "./util.js";
|
||||
import { delay, html_game_list, notify } from './util.js';
|
||||
|
||||
const URL_CLAIM = 'https://gaming.amazon.com/home'; // dummy URL
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const db = await jsonDb('prime-gaming.json', {});
|
|||
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
|
||||
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/pg-${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
|
||||
|
|
@ -44,11 +44,11 @@ try {
|
|||
console.error('Not signed in anymore.');
|
||||
await page.click('button:has-text("Sign in")');
|
||||
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!`);
|
||||
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
||||
if (cfg.pg_email && cfg.pg_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.pg_email || await prompt({message: 'Enter email'});
|
||||
const password = email && (cfg.pg_password || await prompt({type: 'password', message: 'Enter password'}));
|
||||
const email = cfg.pg_email || await prompt({ message: 'Enter email' });
|
||||
const password = email && (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);
|
||||
|
|
@ -66,7 +66,7 @@ try {
|
|||
page.waitForURL('**/ap/mfa**').then(async () => {
|
||||
console.log('Two-Step Verification - enter the One Time Password (OTP), e.g. generated by your Authenticator App');
|
||||
await page.check('[name=rememberDevice]');
|
||||
const otp = cfg.pg_otpkey && authenticator.generate(cfg.pg_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
|
||||
const otp = cfg.pg_otpkey && authenticator.generate(cfg.pg_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=otpCode]').pressSequentially(otp.toString());
|
||||
await page.click('input[type="submit"]');
|
||||
}).catch(_ => { });
|
||||
|
|
@ -128,10 +128,10 @@ try {
|
|||
const slug = await card.locator('a:has-text("Claim")').first().getAttribute('href');
|
||||
const url = 'https://gaming.amazon.com' + slug.split('?')[0];
|
||||
// await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait
|
||||
external_info.push({title, url});
|
||||
external_info.push({ title, url });
|
||||
}
|
||||
for (const {title, url} of external_info) {
|
||||
console.log('Current free game:', title); //, url);
|
||||
for (const { title, url } of external_info) {
|
||||
console.log('Current free game:', title); // , url);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
if (cfg.debug) await page.pause();
|
||||
if (cfg.dryrun) continue;
|
||||
|
|
@ -298,7 +298,7 @@ try {
|
|||
await page.keyboard.press('End'); // scroll to bottom to show all games
|
||||
await page.waitForTimeout(1000); // wait for fade in animation
|
||||
const viewportSize = page.viewportSize(); // current viewport size
|
||||
await page.setViewportSize({...viewportSize, height: 3000}); // increase height, otherwise element screenshot is cut off at the top and bottom
|
||||
await page.setViewportSize({ ...viewportSize, height: 3000 }); // increase height, otherwise element screenshot is cut off at the top and bottom
|
||||
await games.screenshot({ path: p }); // screenshot of all claimed games
|
||||
}
|
||||
|
||||
|
|
@ -357,7 +357,7 @@ try {
|
|||
console.debug(' LinkAccountButton label:', unlinked_store);
|
||||
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?
|
||||
} 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?
|
||||
unlinked_store = 'epic-games';
|
||||
}
|
||||
|
|
@ -386,8 +386,7 @@ try {
|
|||
process.exitCode ||= 1;
|
||||
console.error('--- Exception:');
|
||||
console.error(error); // .toString()?
|
||||
if (error.message && process.exitCode != 130)
|
||||
notify(`prime-gaming failed: ${error.message.split('\n')[0]}`);
|
||||
if (error.message && process.exitCode != 130) notify(`prime-gaming failed: ${error.message.split('\n')[0]}`);
|
||||
} finally {
|
||||
await db.write(); // write out json db
|
||||
if (notify_games.length) { // list should only include claimed games
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
|||
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
|
||||
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-${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
|
||||
|
|
@ -42,7 +42,7 @@ const notify_games = [];
|
|||
let user;
|
||||
|
||||
try {
|
||||
await context.addCookies([{name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5*24*60*60*1000).toISOString(), domain: '.epicgames.com', path: '/'}]); // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
|
||||
await context.addCookies([{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' }]); // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
|
||||
|
||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // 'domcontentloaded' faster than default 'load' https://playwright.dev/docs/api/class-page#page-goto
|
||||
|
||||
|
|
@ -52,12 +52,12 @@ try {
|
|||
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.`);
|
||||
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!`);
|
||||
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
||||
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
||||
if (cfg.eg_email && cfg.eg_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.eg_email || await prompt({message: 'Enter email'});
|
||||
const password = email && (cfg.eg_password || await prompt({type: 'password', message: 'Enter password'}));
|
||||
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);
|
||||
|
|
@ -71,7 +71,7 @@ try {
|
|||
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
|
||||
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(_ => { });
|
||||
|
|
@ -105,7 +105,7 @@ try {
|
|||
const notify_game = { title, url, status: 'failed' };
|
||||
notify_games.push(notify_game); // status is updated below
|
||||
// if (await p.locator('.btn .add-review-btn').count()) { // did not work
|
||||
if((await p.getAttribute('class')).includes('asset--owned')) {
|
||||
if ((await p.getAttribute('class')).includes('asset--owned')) {
|
||||
console.log(' ↳ Already claimed');
|
||||
if (db.data[user][id].status != 'claimed') {
|
||||
db.data[user][id].status = 'existed';
|
||||
|
|
@ -128,7 +128,7 @@ try {
|
|||
const price = (await page.locator('.shopping-cart .total .price').innerText()).split(' ');
|
||||
console.log('Price: ', price[1], 'instead of', price[0]);
|
||||
if (price[1] != '0') {
|
||||
const err = 'Price is not 0! Exit! Please <a href="https://github.com/vogler/free-games-claimer/issues/44">report</a>.'
|
||||
const err = 'Price is not 0! Exit! Please <a href="https://github.com/vogler/free-games-claimer/issues/44">report</a>.';
|
||||
console.error(err);
|
||||
notify('unrealengine: ' + err);
|
||||
process.exit(1);
|
||||
|
|
@ -142,7 +142,7 @@ try {
|
|||
// 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'
|
||||
page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies'
|
||||
}).catch(_ => { });
|
||||
await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed?
|
||||
const iframe = page.frameLocator('#webPurchaseContainer iframe');
|
||||
|
|
@ -165,7 +165,7 @@ try {
|
|||
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.')
|
||||
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) {
|
||||
|
|
@ -192,8 +192,7 @@ try {
|
|||
process.exitCode ||= 1;
|
||||
console.error('--- Exception:');
|
||||
console.error(error); // .toString()?
|
||||
if (error.message && process.exitCode != 130)
|
||||
notify(`unrealengine failed: ${error.message.split('\n')[0]}`);
|
||||
if (error.message && process.exitCode != 130) notify(`unrealengine 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
|
||||
|
|
@ -201,5 +200,5 @@ try {
|
|||
}
|
||||
}
|
||||
if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies()));
|
||||
if (page.video()) console.log('Recorded video:', await page.video().path())
|
||||
if (page.video()) console.log('Recorded video:', await page.video().path());
|
||||
await context.close();
|
||||
|
|
|
|||
19
util.js
19
util.js
|
|
@ -27,7 +27,7 @@ export const handleSIGINT = (context = null) => process.on('SIGINT', async () =>
|
|||
if (context) await context.close(); // in order to save recordings also on SIGINT, we need to disable Playwright's handleSIGINT and close the context ourselves
|
||||
});
|
||||
|
||||
export const stealth = async (context) => {
|
||||
export const stealth = async context => {
|
||||
// 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 = [
|
||||
|
|
@ -47,13 +47,13 @@ export const stealth = async (context) => {
|
|||
'sourceurl',
|
||||
// 'user-agent-override', // doesn't work since playwright has no page.browser()
|
||||
'webgl.vendor',
|
||||
'window.outerdimensions'
|
||||
'window.outerdimensions',
|
||||
];
|
||||
const stealth = {
|
||||
callbacks: [],
|
||||
async evaluateOnNewDocument(...args) {
|
||||
this.callbacks.push({ cb: args[0], a: args[1] });
|
||||
}
|
||||
},
|
||||
};
|
||||
for (const e of enabledEvasions) {
|
||||
const evasion = await import(`puppeteer-extra-plugin-stealth/evasions/${e}/index.js`);
|
||||
|
|
@ -70,7 +70,10 @@ export const stealth = async (context) => {
|
|||
import Enquirer from 'enquirer'; const enquirer = new Enquirer();
|
||||
const timeoutPlugin = timeout => enquirer => { // cancel prompt after timeout ms
|
||||
enquirer.on('prompt', prompt => {
|
||||
const t = setTimeout(() => { prompt.hint = () => 'timeout'; prompt.cancel(); }, timeout);
|
||||
const t = setTimeout(() => {
|
||||
prompt.hint = () => 'timeout';
|
||||
prompt.cancel();
|
||||
}, timeout);
|
||||
prompt.on('submit', _ => clearTimeout(t));
|
||||
prompt.on('cancel', _ => clearTimeout(t));
|
||||
});
|
||||
|
|
@ -78,14 +81,14 @@ const timeoutPlugin = timeout => enquirer => { // cancel prompt after timeout ms
|
|||
enquirer.use(timeoutPlugin(cfg.login_timeout)); // TODO may not want to have this timeout for all prompts; better extend Prompt and add a timeout prompt option
|
||||
// single prompt that just returns the non-empty value instead of an object
|
||||
// @ts-ignore
|
||||
export const prompt = o => enquirer.prompt({name: 'name', type: 'input', message: 'Enter value', ...o}).then(r => r.name).catch(_ => {});
|
||||
export const confirm = o => prompt({type: 'confirm', message: 'Continue?', ...o});
|
||||
export const prompt = o => enquirer.prompt({ name: 'name', type: 'input', message: 'Enter value', ...o }).then(r => r.name).catch(_ => {});
|
||||
export const confirm = o => prompt({ type: 'confirm', message: 'Continue?', ...o });
|
||||
|
||||
// notifications via apprise CLI
|
||||
import { exec } from 'child_process';
|
||||
import { cfg } from './config.js';
|
||||
|
||||
export const notify = (html) => new Promise((resolve, reject) => {
|
||||
export const notify = html => new Promise((resolve, reject) => {
|
||||
if (!cfg.notify) return resolve();
|
||||
const title = cfg.notify_title ? `-t ${cfg.notify_title}` : '';
|
||||
exec(`apprise ${cfg.notify} -i html '${title}' -b '${html}'`, (error, stdout, stderr) => {
|
||||
|
|
@ -102,6 +105,6 @@ export const notify = (html) => new Promise((resolve, reject) => {
|
|||
});
|
||||
});
|
||||
|
||||
export const escapeHtml = (unsafe) => unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||
export const escapeHtml = unsafe => unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('\'', ''');
|
||||
|
||||
export const html_game_list = games => games.map(g => `- <a href="${g.url}">${escapeHtml(g.title)}</a> (${g.status})`).join('<br>');
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// check if running the latest version
|
||||
|
||||
import {log} from 'console';
|
||||
import { log } from 'console';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execp = (cmd) => new Promise((resolve, reject) => {
|
||||
const execp = cmd => new Promise((resolve, reject) => {
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (stderr) console.error(`stderr: ${stderr}`);
|
||||
// if (stdout) console.log(`stdout: ${stdout}`);
|
||||
|
|
@ -36,7 +36,7 @@ if (process.env.NOVNC_PORT) {
|
|||
|
||||
const gh = await (await fetch('https://api.github.com/repos/vogler/free-games-claimer/commits/main', {
|
||||
// headers: { accept: 'application/vnd.github.VERSION.sha' }
|
||||
})).json();
|
||||
})).json();
|
||||
// log(gh);
|
||||
|
||||
log('Local commit:', sha, new Date(date));
|
||||
|
|
|
|||
148
xbox.js
148
xbox.js
|
|
@ -1,5 +1,5 @@
|
|||
import { firefox } from "playwright-firefox"; // stealth plugin needs no outdated playwright-extra
|
||||
import { authenticator } from "otplib";
|
||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
||||
import { authenticator } from 'otplib';
|
||||
import {
|
||||
datetime,
|
||||
handleSIGINT,
|
||||
|
|
@ -7,15 +7,15 @@ import {
|
|||
jsonDb,
|
||||
notify,
|
||||
prompt,
|
||||
} from "./util.js";
|
||||
import { cfg } from "./config.js";
|
||||
} from './util.js';
|
||||
import { cfg } from './config.js';
|
||||
|
||||
// ### SETUP
|
||||
const URL_CLAIM = "https://www.xbox.com/en-US/live/gold"; // #gameswithgold";
|
||||
const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold";
|
||||
|
||||
console.log(datetime(), "started checking xbox");
|
||||
console.log(datetime(), 'started checking xbox');
|
||||
|
||||
const db = await jsonDb("xbox.json");
|
||||
const db = await jsonDb('xbox.json');
|
||||
db.data ||= {};
|
||||
|
||||
handleSIGINT();
|
||||
|
|
@ -24,7 +24,7 @@ handleSIGINT();
|
|||
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
|
||||
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);
|
||||
|
|
@ -46,11 +46,10 @@ async function main() {
|
|||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exitCode ||= 1;
|
||||
if (error.message && process.exitCode != 130)
|
||||
notify(`xbox failed: ${error.message.split("\n")[0]}`);
|
||||
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) {
|
||||
if (notify_games.filter(g => g.status != 'existed').length) {
|
||||
// don't notify if all were already claimed
|
||||
notify(`xbox (${user}):<br>${html_game_list(notify_games)}`);
|
||||
}
|
||||
|
|
@ -59,16 +58,16 @@ async function main() {
|
|||
}
|
||||
|
||||
async function performLogin() {
|
||||
await page.goto(URL_CLAIM, { waitUntil: "domcontentloaded" }); // default 'load' takes forever
|
||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||
|
||||
const signInLocator = page
|
||||
.getByRole("link", {
|
||||
name: "Sign in to your account",
|
||||
.getByRole('link', {
|
||||
name: 'Sign in to your account',
|
||||
})
|
||||
.first();
|
||||
const usernameLocator = page
|
||||
.getByRole("button", {
|
||||
name: "Account manager for",
|
||||
.getByRole('button', {
|
||||
name: 'Account manager for',
|
||||
})
|
||||
.first();
|
||||
|
||||
|
|
@ -77,40 +76,38 @@ async function performLogin() {
|
|||
if (await usernameLocator.isVisible()) {
|
||||
return; // logged in using saved cookie
|
||||
} else if (await signInLocator.isVisible()) {
|
||||
console.error("Not signed in anymore.");
|
||||
console.error('Not signed in anymore.');
|
||||
await signInLocator.click();
|
||||
await signInToXbox();
|
||||
} else {
|
||||
console.error("lost! where am i?");
|
||||
console.error('lost! where am i?');
|
||||
}
|
||||
}
|
||||
|
||||
async function signInToXbox() {
|
||||
page.waitForLoadState("domcontentloaded");
|
||||
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)."
|
||||
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 email = cfg.xbox_email || await prompt({ message: 'Enter email' });
|
||||
const password =
|
||||
email &&
|
||||
(cfg.xbox_password ||
|
||||
(await prompt({
|
||||
type: "password",
|
||||
message: "Enter password",
|
||||
})));
|
||||
await prompt({
|
||||
type: 'password',
|
||||
message: 'Enter password',
|
||||
}));
|
||||
// ### FILL IN EMAIL/PASS
|
||||
if (email && password) {
|
||||
const usernameLocator = page
|
||||
.getByPlaceholder("Email, phone, or Skype")
|
||||
.getByPlaceholder('Email, phone, or Skype')
|
||||
.first();
|
||||
const passwordLocator = page.getByPlaceholder("Password").first();
|
||||
const passwordLocator = page.getByPlaceholder('Password').first();
|
||||
|
||||
await Promise.any([
|
||||
usernameLocator.waitFor(),
|
||||
|
|
@ -118,58 +115,57 @@ async function signInToXbox() {
|
|||
]);
|
||||
|
||||
// username may already be saved from before, if so, skip to filling in password
|
||||
if (await page.getByPlaceholder("Email, phone, or Skype").isVisible()) {
|
||||
if (await page.getByPlaceholder('Email, phone, or Skype').isVisible()) {
|
||||
await usernameLocator.fill(email);
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
}
|
||||
|
||||
await passwordLocator.fill(password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
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('Two-Step Verification - Enter security code');
|
||||
console.log(
|
||||
await page
|
||||
.locator('div[data-bind="text: description"]')
|
||||
.innerText()
|
||||
.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
|
||||
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")
|
||||
.getByLabel('Don\'t ask me again on this device')
|
||||
.check(); // Trust this Browser
|
||||
await page.getByRole("button", { name: "Verify" }).click();
|
||||
await page.getByRole('button', { name: 'Verify' }).click();
|
||||
})
|
||||
.catch((_) => {});
|
||||
.catch(_ => {});
|
||||
|
||||
// Trust this browser, but don't await it
|
||||
page.getByLabel("Don't show this again")
|
||||
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();
|
||||
await page.getByLabel('Don\'t show this again').check();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
})
|
||||
.catch((_) => {});
|
||||
.catch(_ => {});
|
||||
} else {
|
||||
console.log("Waiting for you to login in the browser.");
|
||||
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."
|
||||
'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."
|
||||
'Run `SHOW=1 node xbox` to login in the opened browser.',
|
||||
);
|
||||
await context.close();
|
||||
process.exit(1);
|
||||
|
|
@ -183,35 +179,35 @@ async function signInToXbox() {
|
|||
}
|
||||
|
||||
async function getAndSaveUser() {
|
||||
user = await page.locator("#mectrl_currentAccount_primary").innerHTML();
|
||||
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 monthlyGamesLocator = await page.locator('.f-size-large').all();
|
||||
|
||||
const monthlyGamesPageLinks = await Promise.all(
|
||||
monthlyGamesLocator.map(
|
||||
async (el) => await el.locator("a").getAttribute("href")
|
||||
)
|
||||
async el => await el.locator('a').getAttribute('href'),
|
||||
),
|
||||
);
|
||||
console.log("Free games:", monthlyGamesPageLinks);
|
||||
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();
|
||||
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" };
|
||||
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 getBtnLocator = page.getByText('GET', { exact: true }).first();
|
||||
const installToLocator = page
|
||||
.getByText("INSTALL TO", { exact: true })
|
||||
.getByText('INSTALL TO', { exact: true })
|
||||
.first();
|
||||
|
||||
await Promise.any([
|
||||
|
|
@ -220,11 +216,11 @@ async function redeemFreeGames() {
|
|||
]);
|
||||
|
||||
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
|
||||
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.");
|
||||
console.log(' Not in library yet! Click GET.');
|
||||
await getBtnLocator.click();
|
||||
|
||||
// wait for popup
|
||||
|
|
@ -232,18 +228,18 @@ async function redeemFreeGames() {
|
|||
.locator('iframe[name="purchase-sdk-hosted-iframe"]')
|
||||
.waitFor();
|
||||
const popupLocator = page.frameLocator(
|
||||
"[name=purchase-sdk-hosted-iframe]"
|
||||
'[name=purchase-sdk-hosted-iframe]',
|
||||
);
|
||||
|
||||
const finalGetBtnLocator = popupLocator.getByText("GET");
|
||||
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";
|
||||
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!");
|
||||
console.log(' Claimed successfully!');
|
||||
}
|
||||
|
||||
// notify_game.status = db.data[user][game_id].status; // claimed or failed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue