Clean Sonar issues in store scripts
Some checks failed
build-and-push / lint (push) Failing after 4s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped

This commit is contained in:
nocci 2025-12-30 16:08:59 +00:00
parent 397871b012
commit 5f919039ab
6 changed files with 172 additions and 187 deletions

View file

@ -29,17 +29,13 @@ if (existsSync(browserPrefs)) {
const context = await firefox.launchPersistentContext(cfg.dir.browser, { const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless, headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height }, viewport: { width: cfg.width, height: cfg.height },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // Windows UA avoids "device not supported"; update when browser version changes
// 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 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-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools recordHar: cfg.record ? { path: `data/record/eg-${filenamify(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 handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
// user settings for firefox have to be put in $BROWSER_DIR/user.js // user settings for firefox have to be put in $BROWSER_DIR/user.js
args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions args: [], // https://wiki.mozilla.org/Firefox/CommandLineOptions
// '-kiosk',
],
}); });
handleSIGINT(context); handleSIGINT(context);
@ -50,7 +46,7 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist 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 await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// some debug info about the page (screen dimensions, user agent, platform) // some debug info about the page (screen dimensions, user agent, platform)
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -76,8 +72,6 @@ try {
if (cfg.time) console.timeEnd('startup'); if (cfg.time) console.timeEnd('startup');
if (cfg.time) console.time('login'); if (cfg.time) console.time('login');
// page.click('button:has-text("Accept All Cookies")').catch(_ => { }); // Not needed anymore since we set the cookie above. Clicking this did not always work since the message was animated in too slowly.
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') { while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
console.error('Not signed in anymore. Please login in the browser or here in the terminal.'); 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.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
@ -98,16 +92,20 @@ try {
const email = cfg.eg_email || await prompt({ message: 'Enter email' }); const email = cfg.eg_email || await prompt({ message: 'Enter email' });
if (!email) await notifyBrowserLogin(); if (!email) await notifyBrowserLogin();
else { else {
// await page.click('text=Sign in with Epic Games'); void (async () => {
page.waitForSelector('.h_captcha_challenge iframe').then(async () => { try {
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); await page.waitForSelector('.h_captcha_challenge iframe', { timeout: 15000 });
await notify('epic-games: got captcha during login. Please check.'); console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
}).catch(_ => { }); await notify('epic-games: got captcha during login. Please check.');
page.waitForSelector('p:has-text("Incorrect response.")').then(async () => { } catch {}
console.error('Incorrect response for captcha!'); })();
}).catch(_ => { }); void (async () => {
try {
await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 });
console.error('Incorrect response for captcha!');
} catch {}
})();
await page.fill('#email', email); await page.fill('#email', email);
// await page.click('button[type="submit"]'); login was split in two steps for some time, now email and password are on the same form again
const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' })); const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (!password) await notifyBrowserLogin(); if (!password) await notifyBrowserLogin();
else { else {
@ -115,18 +113,22 @@ try {
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
} }
const error = page.locator('#form-error-message'); const error = page.locator('#form-error-message');
error.waitFor().then(async () => { void (async () => {
console.error('Login error:', await error.innerText()); try {
console.log('Please login in the browser!'); await error.waitFor({ timeout: 15000 });
}).catch(_ => { }); console.error('Login error:', await error.innerText());
// handle MFA, but don't await it console.log('Please login in the browser!');
page.waitForURL('**/id/login/mfa**').then(async () => { } catch {}
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?) void (async () => {
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 try {
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
await page.click('button[type="submit"]'); 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 ...');
}).catch(_ => { }); 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 {}
})();
} }
await page.waitForURL(URL_CLAIM); await page.waitForURL(URL_CLAIM);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
@ -141,7 +143,7 @@ try {
const game_loc = page.locator('a:has(span:text-is("Free Now"))'); const game_loc = page.locator('a:has(span:text-is("Free Now"))');
await game_loc.last().waitFor().catch(_ => { await game_loc.last().waitFor().catch(_ => {
// rarely there are no free games available -> catch Timeout // rarely there are no free games available -> catch Timeout
// TODO would be better to wait for alternative like 'coming soon' instead of waiting for timeout // waiting for timeout; alternative would be waiting for "coming soon"
// see https://github.com/vogler/free-games-claimer/issues/210#issuecomment-1727420943 // see https://github.com/vogler/free-games-claimer/issues/210#issuecomment-1727420943
console.error('Seems like currently there are no free games available in your region...'); console.error('Seems like currently there are no free games available in your region...');
// urls below should then be an empty list // urls below should then be an empty list
@ -208,36 +210,31 @@ try {
console.log(' Requires base game! Nothing to claim.'); console.log(' Requires base game! Nothing to claim.');
notify_game.status = 'requires base game'; notify_game.status = 'requires base game';
db.data[user][game_id].status ||= 'failed:requires-base-game'; db.data[user][game_id].status ||= 'failed:requires-base-game';
// TODO claim base game if it is free // if base game is free, add to queue as well
const baseUrl = 'https://store.epicgames.com' + await page.locator('a:has-text("Overview")').getAttribute('href'); const baseUrl = 'https://store.epicgames.com' + await page.locator('a:has-text("Overview")').getAttribute('href');
console.log(' Base game:', baseUrl); console.log(' Base game:', baseUrl);
// await page.click('a:has-text("Overview")'); // await page.click('a:has-text("Overview")');
// TODO handle this via function call for base game above since this will never terminate if DRYRUN=1 // re-add original add-on to queue after base game
urls.push(baseUrl); // add base game to the list of games to claim urls.push(baseUrl); // add base game to the list of games to claim
urls.push(url); // add add-on itself again urls.push(url); // add add-on itself again
} else { // GET } else { // GET
console.log(' Not in library yet! Click', btnText); console.log(' Not in library yet! Click', btnText);
await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
// 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?
// click 'Yes, buy now' if 'This edition contains something you already have. Still interested?'
page.click('button:has-text("Yes, buy now")').catch(_ => { });
// Accept End User License Agreement (only needed once) // Accept End User License Agreement (only needed once)
page.locator(':has-text("end user license agreement")').waitFor().then(async () => { void (async () => {
console.log(' Accept End User License Agreement (only needed once)'); try {
console.log(page.innerHTML); await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 });
console.log('Please report the HTML above here: https://github.com/vogler/free-games-claimer/issues/371'); console.log(' Accept End User License Agreement (only needed once)');
await page.locator('input#agree').check(); // TODO Bundle: got stuck here; likely unrelated to bundle and locator just changed: https://github.com/vogler/free-games-claimer/issues/371 await page.locator('input#agree').check();
await page.locator('button:has-text("Accept")').click(); await page.locator('button:has-text("Accept")').click();
}).catch(_ => { }); } catch {}
})();
// it then creates an iframe for the purchase // it then creates an iframe for the purchase
await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? await page.waitForSelector('#webPurchaseContainer iframe');
const iframe = page.frameLocator('#webPurchaseContainer iframe'); const iframe = page.frameLocator('#webPurchaseContainer iframe');
// skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46 TODO check games for account's region // skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46
if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) { if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) {
console.error(' This product is unavailable in your region!'); console.error(' This product is unavailable in your region!');
db.data[user][game_id].status = notify_game.status = 'unavailable-in-region'; db.data[user][game_id].status = notify_game.status = 'unavailable-in-region';
@ -245,14 +242,17 @@ try {
continue; continue;
} }
iframe.locator('.payment-pin-code').waitFor().then(async () => { void (async () => {
if (!cfg.eg_parentalpin) { try {
console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 });
notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); if (!cfg.eg_parentalpin) {
} console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin); notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); }
}).catch(_ => { }); await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin);
await iframe.locator('button:has-text("Continue")').click({ delay: 11 });
} catch {}
})();
if (cfg.debug) await page.pause(); if (cfg.debug) await page.pause();
if (cfg.dryrun) { if (cfg.dryrun) {
@ -267,27 +267,30 @@ try {
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872 // I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
const btnAgree = iframe.locator('button:has-text("I Accept")'); const btnAgree = iframe.locator('button:has-text("I Accept")');
btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' void (async () => {
try {
await btnAgree.waitFor({ timeout: 10000 });
await btnAgree.click();
} catch {}
})(); // EU: wait for and click 'I Agree'
try { try {
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s? // context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe'); const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe');
captcha.waitFor().then(async () => { // don't await, since element may not be shown void (async () => {
// console.info(' Got hcaptcha challenge! NopeCHA extension will likely solve it.') try {
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.'); await captcha.waitFor({ timeout: 10000 });
// await notify(`epic-games: got captcha challenge right before claim of <a href="${url}">${title}</a>. Use VNC to solve it manually.`); // TODO not all apprise services understand HTML: https://github.com/vogler/free-games-claimer/pull/417 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.');
await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`); await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`);
// TODO could even create purchase URL, see https://github.com/vogler/free-games-claimer/pull/130 } catch {}
// await page.waitForTimeout(2000); })(); // may time out if not shown
// const p = path.resolve(cfg.dir.screenshots, 'epic-games', 'captcha', `${filenamify(datetime())}.png`); void (async () => {
// await captcha.screenshot({ path: p }); try {
// console.info(' Saved a screenshot of hcaptcha challenge to', p); await iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor({ timeout: 10000 });
// 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? console.error(' Failed to challenge captcha, please try again later.');
}).catch(_ => { }); // may time out if not shown await notify('epic-games: failed to challenge captcha. Please check.');
iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor().then(async () => { } catch {}
console.error(' Failed to challenge captcha, please try again later.'); })();
await notify('epic-games: failed to challenge captcha. Please check.'); await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' });
}).catch(_ => { });
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here, but normal game now as well
db.data[user][game_id].status = 'claimed'; db.data[user][game_id].status = 'claimed';
db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time
console.log(' Claimed successfully!'); console.log(' Claimed successfully!');

39
gog.js
View file

@ -31,7 +31,7 @@ handleSIGINT(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist 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 await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
const notify_games = []; const notify_games = [];
let user; let user;
@ -47,7 +47,7 @@ try {
console.error('Not signed in anymore.'); console.error('Not signed in anymore.');
await signIn.click(); await signIn.click();
// it then creates an iframe for the login // it then creates an iframe for the login
await page.waitForSelector('#GalaxyAccountsFrameContainer iframe'); // TODO needed? await page.waitForSelector('#GalaxyAccountsFrameContainer iframe');
const iframe = page.frameLocator('#GalaxyAccountsFrameContainer iframe'); const iframe = page.frameLocator('#GalaxyAccountsFrameContainer iframe');
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log 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!`);
@ -60,19 +60,24 @@ try {
await iframe.locator('#login_username').fill(email); await iframe.locator('#login_username').fill(email);
await iframe.locator('#login_password').fill(password); await iframe.locator('#login_password').fill(password);
await iframe.locator('#login_login').click(); await iframe.locator('#login_login').click();
// handle MFA, but don't await it void (async () => {
iframe.locator('form[name=second_step_authentication]').waitFor().then(async () => { try {
console.log('Two-Step Verification - Enter security code'); await iframe.locator('form[name=second_step_authentication]').waitFor({ timeout: 15000 });
console.log(await iframe.locator('.form__description').innerText()); console.log('Two-Step Verification - Enter security code');
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 console.log(await iframe.locator('.form__description').innerText());
await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), { delay: 10 }); 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_send').click(); await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), { delay: 10 });
await page.waitForTimeout(1000); // TODO still needed with wait for username below? await iframe.locator('#second_step_authentication_send').click();
}).catch(_ => { }); await page.waitForTimeout(1000);
iframe.locator('text=Invalid captcha').waitFor().then(() => { } catch {}
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); })();
notify('gog: got captcha during login. Please check.'); void (async () => {
}).catch(_ => { }); try {
await iframe.locator('text=Invalid captcha').waitFor({ timeout: 15000 });
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
notify('gog: got captcha during login. Please check.');
} catch {}
})();
await page.waitForSelector('#menuUsername'); await page.waitForSelector('#menuUsername');
} else { } else {
console.log('Waiting for you to login in the browser.'); console.log('Waiting for you to login in the browser.');
@ -101,11 +106,9 @@ try {
console.log(`Current free game: ${chalk.blue(title)} - ${url}`); console.log(`Current free game: ${chalk.blue(title)} - ${url}`);
db.data[user][title] ||= { title, time: datetime(), url }; db.data[user][title] ||= { title, time: datetime(), url };
if (cfg.dryrun) process.exit(1); if (cfg.dryrun) process.exit(1);
// await page.locator('#giveaway:not(.is-loading)').waitFor(); // otherwise screenshot is sometimes with loading indicator instead of game title; #TODO fix, skipped due to timeout, see #240
await banner.screenshot({ path: screenshot(`${filenamify(title)}.png`) }); // overwrites every time - only keep first? await banner.screenshot({ path: screenshot(`${filenamify(title)}.png`) }); // overwrites every time - only keep first?
// await banner.getByRole('button', { name: 'Add to library' }).click(); // instead of clicking the button, visit the auto-claim URL which gives a JSON response
// instead of clicking the button, we visit the auto-claim URL which gives as a JSON response which is easier than checking the state of a button
await page.goto('https://www.gog.com/giveaway/claim'); await page.goto('https://www.gog.com/giveaway/claim');
const response = await page.innerText('body'); const response = await page.innerText('body');
// console.log(response); // console.log(response);

View file

@ -6,7 +6,6 @@ import { cfg } from './src/config.js';
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a); const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a);
// const URL_LOGIN = 'https://www.amazon.de/ap/signin'; // wrong. needs some session args to be valid?
const URL_CLAIM = 'https://luna.amazon.com/claims/home'; const URL_CLAIM = 'https://luna.amazon.com/claims/home';
console.log(datetime(), 'started checking prime-gaming'); console.log(datetime(), 'started checking prime-gaming');
@ -25,14 +24,12 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
handleSIGINT(context); handleSIGINT(context);
// TODO test if needed
await stealth(context); await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist 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 await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const notify_games = []; const notify_games = [];
let user; let user;
@ -62,14 +59,18 @@ try {
await page.fill('[name=password]', password); await page.fill('[name=password]', password);
await page.click('input[type="submit"]'); await page.click('input[type="submit"]');
await handleMFA(page).catch(() => {}); await handleMFA(page).catch(() => {});
page.waitForURL('**/ap/signin**').then(async () => { // check for wrong credentials try {
await page.waitForURL('**/ap/signin**');
const error = await page.locator('.a-alert-content').first().innerText(); const error = await page.locator('.a-alert-content').first().innerText();
if (!error.trim.length) return; if (error.trim().length) {
console.error('Login error:', error); console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`); await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording await context.close(); // finishes potential recording
process.exit(1); process.exit(1);
}); }
} catch {
// if navigation succeeded, continue
}
await page.waitForURL(/luna\.amazon\.com\/claims\/.*signedIn=true/); await page.waitForURL(/luna\.amazon\.com\/claims\/.*signedIn=true/);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
return true; return true;
@ -91,7 +92,7 @@ try {
'[data-a-target="user-dropdown-first-name-text"]', '[data-a-target="user-dropdown-first-name-text"]',
'[data-testid="user-dropdown-first-name-text"]', '[data-testid="user-dropdown-first-name-text"]',
].map(s => page.waitForSelector(s))); ].map(s => page.waitForSelector(s)));
page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(() => { }); // to not waste screen space when non-headless, TODO does not work reliably, need to wait for something else first? page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(() => { }); // to not waste screen space when non-headless; could be flaky
while (await page.locator('button:has-text("Sign in"), button:has-text("Anmelden")').count() > 0) { while (await page.locator('button:has-text("Sign in"), button:has-text("Anmelden")').count() > 0) {
console.error('Not signed in anymore.'); console.error('Not signed in anymore.');
await page.click('button:has-text("Sign in")'); await page.click('button:has-text("Sign in")');
@ -105,16 +106,19 @@ try {
await page.fill('[name=email]', email); await page.fill('[name=email]', email);
await page.click('input[type="submit"]'); await page.click('input[type="submit"]');
await page.fill('[name=password]', password); await page.fill('[name=password]', password);
// await page.check('[name=rememberMe]'); // no longer exists
await page.click('input[type="submit"]'); await page.click('input[type="submit"]');
page.waitForURL('**/ap/signin**').then(async () => { // check for wrong credentials try {
await page.waitForURL('**/ap/signin**');
const error = await page.locator('.a-alert-content').first().innerText(); const error = await page.locator('.a-alert-content').first().innerText();
if (!error.trim.length) return; if (error.trim().length) {
console.error('Login error:', error); console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`); await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording await context.close(); // finishes potential recording
process.exit(1); process.exit(1);
}); }
} catch {
// navigation ok
}
handleMFA(page).catch(() => {}); handleMFA(page).catch(() => {});
} else { } else {
console.log('Waiting for you to login in the browser.'); console.log('Waiting for you to login in the browser.');
@ -130,9 +134,6 @@ try {
} }
user = await page.locator('[data-a-target="user-dropdown-first-name-text"], [data-testid="user-dropdown-first-name-text"]').first().innerText(); user = await page.locator('[data-a-target="user-dropdown-first-name-text"], [data-testid="user-dropdown-first-name-text"]').first().innerText();
console.log(`Signed in as ${user}`); console.log(`Signed in as ${user}`);
// await page.click('button[aria-label="User dropdown and more options"]');
// const twitch = await page.locator('[data-a-target="TwitchDisplayName"]').first().innerText();
// console.log(`Twitch user name is ${twitch}`);
db.data[user] ||= {}; db.data[user] ||= {};
if (await page.getByRole('button', { name: 'Try Prime' }).count()) { if (await page.getByRole('button', { name: 'Try Prime' }).count()) {
@ -156,7 +157,7 @@ try {
// loading all games became flaky; see https://github.com/vogler/free-games-claimer/issues/357 // loading all games became flaky; see https://github.com/vogler/free-games-claimer/issues/357
await page.keyboard.press('PageDown'); // scrolling to straight to the bottom started to skip loading some games await page.keyboard.press('PageDown'); // scrolling to straight to the bottom started to skip loading some games
await page.waitForLoadState('networkidle'); // wait for all games to be loaded await page.waitForLoadState('networkidle'); // wait for all games to be loaded
await page.waitForTimeout(3000); // TODO networkidle wasn't enough to load all already collected games await page.waitForTimeout(3000); // extra wait to load all already collected games
// do it again since once wasn't enough... // do it again since once wasn't enough...
await page.keyboard.press('PageDown'); await page.keyboard.press('PageDown');
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
@ -372,9 +373,9 @@ try {
for (const { title, url } of external_info) { for (const { title, url } of external_info) {
console.log('Current free game:', chalk.blue(title)); // , url); console.log('Current free game:', chalk.blue(title)); // , url);
const existing = db.data[user]?.[title]; const existingStatus = db.data[user]?.[title]?.status;
if (existing && existing.status && !existing.status.startsWith('failed')) { if (existingStatus && !existingStatus.startsWith('failed')) {
console.log(` Already recorded as ${existing.status}, skipping.`); console.log(` Already recorded as ${existingStatus}, skipping.`);
notify_games.push({ title, url, status: 'existed' }); notify_games.push({ title, url, status: 'existed' });
continue; continue;
} }
@ -448,21 +449,21 @@ try {
page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(() => {}), page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(() => {}),
]).catch(() => {}); ]).catch(() => {});
db.data[user][title] ||= { title, time: datetime(), url, store }; db.data[user][title] ||= { title, time: datetime(), url, store };
if (await page.locator('div:has-text("Link game account")').count() // TODO still needed? epic games store just has 'Link account' as the button text now. if (await page.locator('div:has-text("Link game account")').count() // epic games store also shows "Link account"
|| await page.locator('div:has-text("Link account")').count()) { || await page.locator('div:has-text("Link account")').count()) {
console.error(' Account linking is required to claim this offer!'); console.error(' Account linking is required to claim this offer!');
notify_game.status = `failed: need account linking for ${store}`; notify_game.status = `failed: need account linking for ${store}`;
db.data[user][title].status = 'failed: need account linking'; db.data[user][title].status = 'failed: need account linking';
// await page.pause(); // await page.pause();
// await page.click('[data-a-target="LinkAccountModal"] [data-a-target="LinkAccountButton"]'); // await page.click('[data-a-target="LinkAccountModal"] [data-a-target="LinkAccountButton"]');
// TODO login for epic games also needed if already logged in // login for epic games also needed if already logged in
// wait for https://www.epicgames.com/id/authorize?redirect_uri=https%3A%2F%2Fservice.link.amazon.gg... // wait for https://www.epicgames.com/id/authorize?redirect_uri=https%3A%2F%2Fservice.link.amazon.gg...
// await page.click('button[aria-label="Allow"]'); // await page.click('button[aria-label="Allow"]');
} else { } else {
db.data[user][title].status = 'claimed'; db.data[user][title].status = 'claimed';
// print code if there is one // print code if there is one
const redeem = { const redeem = {
// 'origin': 'https://www.origin.com/redeem', // TODO still needed or now only via account linking? // 'origin': 'https://www.origin.com/redeem', // kept for legacy flows; current path uses account linking
'gog.com': 'https://www.gog.com/redeem', 'gog.com': 'https://www.gog.com/redeem',
'microsoft store': 'https://account.microsoft.com/billing/redeem', 'microsoft store': 'https://account.microsoft.com/billing/redeem',
xbox: 'https://account.microsoft.com/billing/redeem', xbox: 'https://account.microsoft.com/billing/redeem',
@ -519,7 +520,7 @@ try {
} else if (reason == 'code_not_found') { } else if (reason == 'code_not_found') {
redeem_action = 'redeem (not found)'; redeem_action = 'redeem (not found)';
console.error(' Code was not found!'); console.error(' Code was not found!');
} else { // TODO not logged in? need valid unused code to test. } else { // unknown state; keep info log for later analysis
redeem_action = 'redeemed?'; redeem_action = 'redeemed?';
// console.log(' Redeemed successfully? Please report your Responses (if new) in https://github.com/vogler/free-games-claimer/issues/5'); // console.log(' Redeemed successfully? Please report your Responses (if new) in https://github.com/vogler/free-games-claimer/issues/5');
console.debug(` Response 1: ${r1t}`); console.debug(` Response 1: ${r1t}`);
@ -569,12 +570,12 @@ try {
if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') { if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') {
redeem_action = 'already redeemed'; redeem_action = 'already redeemed';
console.error(' error: UserAlreadyOwnsContent'); console.error(' error: UserAlreadyOwnsContent');
} else { // TODO what's returned on success? } else { // success path not seen yet; log below if needed
redeem_action = 'redeemed'; redeem_action = 'redeemed';
db.data[user][title].status = 'claimed and redeemed?'; db.data[user][title].status = 'claimed and redeemed?';
console.log(' Redeemed successfully? Please report if not in https://github.com/vogler/free-games-claimer/issues/5'); console.log(' Redeemed successfully? Please report if not in https://github.com/vogler/free-games-claimer/issues/5');
} }
} else { // TODO find out other responses } else { // other responses; keep info log for analysis
redeem_action = 'unknown'; redeem_action = 'unknown';
console.debug(` Response: ${rt}`); console.debug(` Response: ${rt}`);
console.log(' Redeemed successfully? Please report your Response from above (if it is new) in https://github.com/vogler/free-games-claimer/issues/5'); console.log(' Redeemed successfully? Please report your Response from above (if it is new) in https://github.com/vogler/free-games-claimer/issues/5');
@ -672,7 +673,7 @@ try {
const match = unlinked_store.match(/Link (.*) account/); const match = unlinked_store.match(/Link (.*) account/);
if (match && match.length == 2) unlinked_store = match[1]; 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? console.error(' Missing account linking (epic-games specific button?):', await page.locator('button[data-a-target="gms-cta"]').innerText()); // track account-linking UI drift
unlinked_store = 'epic-games'; unlinked_store = 'epic-games';
} }
if (unlinked_store) { if (unlinked_store) {

View file

@ -18,7 +18,6 @@ const datetime_UTCtoLocalTimezone = async file => {
db.data[user][game].time = time2; db.data[user][game].time = time2;
} }
} }
// console.log(db.data);
await db.write(); // write out json db await db.write(); // write out json db
}; };

View file

@ -1,36 +1,13 @@
// https://github.com/enquirer/enquirer/issues/372 // https://github.com/enquirer/enquirer/issues/372
import { prompt, handleSIGINT } from '../src/util.js'; import { prompt, handleSIGINT } from '../src/util.js';
// const handleSIGINT = () => process.on('SIGINT', () => { // e.g. when killed by Ctrl-C
// console.log('\nInterrupted by SIGINT. Exit!');
// process.exitCode = 130;
// });
handleSIGINT(); handleSIGINT();
// function onRawSIGINT(fn) {
// const { stdin, stdout } = process;
// stdin.setRawMode(true);
// stdin.resume();
// stdin.on('data', data => {
// const key = data.toString('utf-8');
// if (key === '\u0003') { // ctrl + c
// fn();
// } else {
// stdout.write(key);
// }
// });
// }
// onRawSIGINT(() => {
// console.log('raw'); process.exit(1);
// });
console.log('hello'); console.log('hello');
console.error('hello error'); console.error('hello error');
try { try {
let i = 'foo'; let i = await prompt(); // SIGINT no longer handled if this is executed
i = await prompt(); // SIGINT no longer handled if this is executed i = await prompt(); // SIGINT no longer handled if this is executed
i = await prompt(); // SIGINT no longer handled if this is executed
// handleSIGINT();
console.log('value:', i); console.log('value:', i);
setTimeout(() => console.log('timeout 3s'), 3000); setTimeout(() => console.log('timeout 3s'), 3000);
} catch (e) { } catch (e) {

View file

@ -1,6 +1,3 @@
// TODO This is mostly a copy of epic-games.js
// New assets to claim every first Tuesday of a month.
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib'; import { authenticator } from 'otplib';
import path from 'node:path'; import path from 'node:path';
@ -21,8 +18,7 @@ const db = await jsonDb('unrealengine.json', {});
const context = await firefox.launchPersistentContext(cfg.dir.browser, { const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless, headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height }, 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: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // Windows UA avoids "device not supported"; update when browser version changes
// 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 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-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools recordHar: cfg.record ? { path: `data/record/ue-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
@ -36,8 +32,7 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist 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 await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const notify_games = []; const notify_games = [];
let user; let user;
@ -60,23 +55,26 @@ try {
const email = cfg.eg_email || await prompt({ message: 'Enter email' }); const email = cfg.eg_email || await prompt({ message: 'Enter email' });
const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' })); const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) { if (email && password) {
// await page.click('text=Sign in with Epic Games');
await page.fill('#email', email); await page.fill('#email', email);
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await page.fill('#password', password); await page.fill('#password', password);
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
page.waitForSelector('#h_captcha_challenge_login_prod iframe').then(() => { void (async () => {
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.'); try {
notify('unrealengine: got captcha during login. Please check.'); await page.waitForSelector('#h_captcha_challenge_login_prod iframe', { timeout: 15000 });
}).catch(_ => { }); console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
// handle MFA, but don't await it notify('unrealengine: got captcha during login. Please check.');
page.waitForURL('**/id/login/mfa**').then(async () => { } catch {}
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?) void (async () => {
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 try {
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
await page.click('button[type="submit"]'); 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 ...');
}).catch(_ => { }); 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 {}
})();
} else { } else {
console.log('Waiting for you to login in the browser.'); console.log('Waiting for you to login in the browser.');
await notify('unrealengine: no longer signed in and not enough options set for automatic login.'); await notify('unrealengine: no longer signed in and not enough options set for automatic login.');
@ -135,18 +133,19 @@ try {
notify('unrealengine: ' + err); notify('unrealengine: ' + err);
process.exit(1); process.exit(1);
} }
// await page.pause();
console.log('Click shopping cart'); console.log('Click shopping cart');
await page.locator('.shopping-cart').click(); await page.locator('.shopping-cart').click();
// await page.waitForTimeout(2000);
await page.locator('button.checkout').click(); await page.locator('button.checkout').click();
console.log('Click checkout'); console.log('Click checkout');
// maybe: Accept End User License Agreement // maybe: Accept End User License Agreement
page.locator('[name=accept-label]').check().then(() => { void (async () => {
console.log('Accept End User License Agreement'); try {
page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies' await page.locator('[name=accept-label]').check({ timeout: 10000 });
}).catch(_ => { }); console.log('Accept End User License Agreement');
await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed? await page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies'
} catch {}
})();
await page.waitForSelector('#webPurchaseContainer iframe');
const iframe = page.frameLocator('#webPurchaseContainer iframe'); const iframe = page.frameLocator('#webPurchaseContainer iframe');
if (cfg.debug) await page.pause(); if (cfg.debug) await page.pause();
@ -161,14 +160,21 @@ try {
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872 // I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
const btnAgree = iframe.locator('button:has-text("I Agree")'); const btnAgree = iframe.locator('button:has-text("I Agree")');
btnAgree.waitFor().then(() => btnAgree.click()).catch(_ => { }); // EU: wait for and click 'I Agree' void (async () => {
try {
await btnAgree.waitFor({ timeout: 10000 });
await btnAgree.click();
} catch {}
})(); // EU: wait for and click 'I Agree'
try { try {
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s? // context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe'); const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe');
captcha.waitFor().then(async () => { // don't await, since element may not be shown void (async () => {
// console.info(' Got hcaptcha challenge! NopeCHA extension will likely solve it.') try {
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.'); await captcha.waitFor({ timeout: 10000 });
}).catch(_ => { }); // may time out if not shown 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'); await page.waitForSelector('text=Thank you');
for (const id of ids) { for (const id of ids) {
db.data[user][id].status = 'claimed'; db.data[user][id].status = 'claimed';
@ -176,16 +182,12 @@ try {
} }
notify_games.forEach(g => g.status == 'failed' && (g.status = 'claimed')); notify_games.forEach(g => g.status == 'failed' && (g.status = 'claimed'));
console.log('Claimed successfully!'); console.log('Claimed successfully!');
// context.setDefaultTimeout(cfg.timeout);
} catch (e) { } catch (e) {
console.log(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');
console.error(' Failed to claim! To avoid captchas try to get a new IP address.'); console.error(' Failed to claim! To avoid captchas try to get a new IP address.');
await page.screenshot({ path: screenshot('failed', `${filenamify(datetime())}.png`), fullPage: true }); await page.screenshot({ path: screenshot('failed', `${filenamify(datetime())}.png`), fullPage: true });
// db.data[user][id].status = 'failed';
notify_games.forEach(g => g.status = 'failed'); notify_games.forEach(g => g.status = 'failed');
} }
// notify_game.status = db.data[user][game_id].status; // claimed or failed
if (notify_games.length) await page.screenshot({ path: screenshot(`${filenamify(datetime())}.png`), fullPage: false }); // fullPage is quite long... if (notify_games.length) await page.screenshot({ path: screenshot(`${filenamify(datetime())}.png`), fullPage: false }); // fullPage is quite long...
console.log('Done'); console.log('Done');