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, {
headless: cfg.headless,
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 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
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
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-${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
// user settings for firefox have to be put in $BROWSER_DIR/user.js
args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions
// '-kiosk',
],
args: [], // https://wiki.mozilla.org/Firefox/CommandLineOptions
});
handleSIGINT(context);
@ -50,7 +46,7 @@ await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
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)
// eslint-disable-next-line no-undef
@ -76,8 +72,6 @@ try {
if (cfg.time) console.timeEnd('startup');
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') {
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.`);
@ -98,16 +92,20 @@ try {
const email = cfg.eg_email || await prompt({ message: 'Enter email' });
if (!email) await notifyBrowserLogin();
else {
// await page.click('text=Sign in with Epic Games');
page.waitForSelector('.h_captcha_challenge iframe').then(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.');
await notify('epic-games: got captcha during login. Please check.');
}).catch(_ => { });
page.waitForSelector('p:has-text("Incorrect response.")').then(async () => {
console.error('Incorrect response for captcha!');
}).catch(_ => { });
void (async () => {
try {
await page.waitForSelector('.h_captcha_challenge iframe', { 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.');
await notify('epic-games: got captcha during login. Please check.');
} 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.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' }));
if (!password) await notifyBrowserLogin();
else {
@ -115,18 +113,22 @@ try {
await page.click('button[type="submit"]');
}
const error = page.locator('#form-error-message');
error.waitFor().then(async () => {
console.error('Login error:', await error.innerText());
console.log('Please login in the browser!');
}).catch(_ => { });
// handle MFA, but don't await it
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
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
await page.click('button[type="submit"]');
}).catch(_ => { });
void (async () => {
try {
await error.waitFor({ timeout: 15000 });
console.error('Login error:', await error.innerText());
console.log('Please login in the browser!');
} catch {}
})();
void (async () => {
try {
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
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 ...');
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);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
@ -141,7 +143,7 @@ try {
const game_loc = page.locator('a:has(span:text-is("Free Now"))');
await game_loc.last().waitFor().catch(_ => {
// 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
console.error('Seems like currently there are no free games available in your region...');
// urls below should then be an empty list
@ -208,36 +210,31 @@ try {
console.log(' Requires base game! Nothing to claim.');
notify_game.status = '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');
console.log(' Base game:', baseUrl);
// 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(url); // add add-on itself again
} else { // GET
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
// 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)
page.locator(':has-text("end user license agreement")').waitFor().then(async () => {
console.log(' Accept End User License Agreement (only needed once)');
console.log(page.innerHTML);
console.log('Please report the HTML above here: https://github.com/vogler/free-games-claimer/issues/371');
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('button:has-text("Accept")').click();
}).catch(_ => { });
void (async () => {
try {
await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 });
console.log(' Accept End User License Agreement (only needed once)');
await page.locator('input#agree').check();
await page.locator('button:has-text("Accept")').click();
} catch {}
})();
// 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');
// 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) {
console.error(' This product is unavailable in your region!');
db.data[user][game_id].status = notify_game.status = 'unavailable-in-region';
@ -245,14 +242,17 @@ try {
continue;
}
iframe.locator('.payment-pin-code').waitFor().then(async () => {
if (!cfg.eg_parentalpin) {
console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
}
await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin);
await iframe.locator('button:has-text("Continue")').click({ delay: 11 });
}).catch(_ => { });
void (async () => {
try {
await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 });
if (!cfg.eg_parentalpin) {
console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
}
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.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
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 {
// 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');
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.');
// 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
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
// await page.waitForTimeout(2000);
// const p = path.resolve(cfg.dir.screenshots, 'epic-games', 'captcha', `${filenamify(datetime())}.png`);
// await captcha.screenshot({ path: p });
// 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
iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor().then(async () => {
console.error(' Failed to challenge captcha, please try again later.');
await notify('epic-games: failed to challenge captcha. Please check.');
}).catch(_ => { });
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here, but normal game now as well
void (async () => {
try {
await captcha.waitFor({ timeout: 10000 });
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}`);
} catch {}
})(); // may time out if not shown
void (async () => {
try {
await iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor({ timeout: 10000 });
console.error(' Failed to challenge captcha, please try again later.');
await notify('epic-games: failed to challenge captcha. Please check.');
} catch {}
})();
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!');

39
gog.js
View file

@ -31,7 +31,7 @@ handleSIGINT(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
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 = [];
let user;
@ -47,7 +47,7 @@ try {
console.error('Not signed in anymore.');
await signIn.click();
// 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');
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!`);
@ -60,19 +60,24 @@ try {
await iframe.locator('#login_username').fill(email);
await iframe.locator('#login_password').fill(password);
await iframe.locator('#login_login').click();
// 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 });
await iframe.locator('#second_step_authentication_send').click();
await page.waitForTimeout(1000); // TODO still needed with wait for username below?
}).catch(_ => { });
iframe.locator('text=Invalid captcha').waitFor().then(() => {
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(_ => { });
void (async () => {
try {
await iframe.locator('form[name=second_step_authentication]').waitFor({ timeout: 15000 });
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 });
await iframe.locator('#second_step_authentication_send').click();
await page.waitForTimeout(1000);
} catch {}
})();
void (async () => {
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');
} else {
console.log('Waiting for you to login in the browser.');
@ -101,11 +106,9 @@ try {
console.log(`Current free game: ${chalk.blue(title)} - ${url}`);
db.data[user][title] ||= { title, time: datetime(), url };
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.getByRole('button', { name: 'Add to library' }).click();
// 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
// instead of clicking the button, visit the auto-claim URL which gives a JSON response
await page.goto('https://www.gog.com/giveaway/claim');
const response = await page.innerText('body');
// 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 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';
console.log(datetime(), 'started checking prime-gaming');
@ -25,14 +24,12 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
handleSIGINT(context);
// TODO test if needed
await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // TODO workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
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 = [];
let user;
@ -62,14 +59,18 @@ try {
await page.fill('[name=password]', password);
await page.click('input[type="submit"]');
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();
if (!error.trim.length) return;
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
});
if (error.trim().length) {
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
}
} catch {
// if navigation succeeded, continue
}
await page.waitForURL(/luna\.amazon\.com\/claims\/.*signedIn=true/);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
return true;
@ -91,7 +92,7 @@ try {
'[data-a-target="user-dropdown-first-name-text"]',
'[data-testid="user-dropdown-first-name-text"]',
].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) {
console.error('Not signed in anymore.');
await page.click('button:has-text("Sign in")');
@ -105,16 +106,19 @@ try {
await page.fill('[name=email]', email);
await page.click('input[type="submit"]');
await page.fill('[name=password]', password);
// await page.check('[name=rememberMe]'); // no longer exists
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();
if (!error.trim.length) return;
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
});
if (error.trim().length) {
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
}
} catch {
// navigation ok
}
handleMFA(page).catch(() => {});
} else {
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();
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] ||= {};
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
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.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...
await page.keyboard.press('PageDown');
await page.waitForTimeout(3000);
@ -372,9 +373,9 @@ try {
for (const { title, url } of external_info) {
console.log('Current free game:', chalk.blue(title)); // , url);
const existing = db.data[user]?.[title];
if (existing && existing.status && !existing.status.startsWith('failed')) {
console.log(` Already recorded as ${existing.status}, skipping.`);
const existingStatus = db.data[user]?.[title]?.status;
if (existingStatus && !existingStatus.startsWith('failed')) {
console.log(` Already recorded as ${existingStatus}, skipping.`);
notify_games.push({ title, url, status: 'existed' });
continue;
}
@ -448,21 +449,21 @@ try {
page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(() => {}),
]).catch(() => {});
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()) {
console.error(' Account linking is required to claim this offer!');
notify_game.status = `failed: need account linking for ${store}`;
db.data[user][title].status = 'failed: need account linking';
// await page.pause();
// 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...
// await page.click('button[aria-label="Allow"]');
} else {
db.data[user][title].status = 'claimed';
// print code if there is one
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',
'microsoft store': 'https://account.microsoft.com/billing/redeem',
xbox: 'https://account.microsoft.com/billing/redeem',
@ -519,7 +520,7 @@ try {
} else if (reason == 'code_not_found') {
redeem_action = 'redeem (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?';
// 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}`);
@ -569,12 +570,12 @@ try {
if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') {
redeem_action = 'already redeemed';
console.error(' error: UserAlreadyOwnsContent');
} else { // TODO what's returned on success?
} else { // success path not seen yet; log below if needed
redeem_action = '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');
}
} else { // TODO find out other responses
} else { // other responses; keep info log for analysis
redeem_action = 'unknown';
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');
@ -672,7 +673,7 @@ try {
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?
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';
}
if (unlinked_store) {

View file

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

View file

@ -1,36 +1,13 @@
// https://github.com/enquirer/enquirer/issues/372
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();
// 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.error('hello error');
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
// handleSIGINT();
console.log('value:', i);
setTimeout(() => console.log('timeout 3s'), 3000);
} 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 { authenticator } from 'otplib';
import path from 'node:path';
@ -21,8 +18,7 @@ const db = await jsonDb('unrealengine.json', {});
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated?
// userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0
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
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-${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);
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
// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
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 = [];
let user;
@ -60,23 +55,26 @@ try {
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);
await page.click('button[type="submit"]');
await page.fill('#password', password);
await page.click('button[type="submit"]');
page.waitForSelector('#h_captcha_challenge_login_prod iframe').then(() => {
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('unrealengine: got captcha during login. Please check.');
}).catch(_ => { });
// handle MFA, but don't await it
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
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
await page.click('button[type="submit"]');
}).catch(_ => { });
void (async () => {
try {
await page.waitForSelector('#h_captcha_challenge_login_prod iframe', { 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('unrealengine: got captcha during login. Please check.');
} catch {}
})();
void (async () => {
try {
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
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 ...');
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 {
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.');
@ -135,18 +133,19 @@ try {
notify('unrealengine: ' + err);
process.exit(1);
}
// await page.pause();
console.log('Click shopping cart');
await page.locator('.shopping-cart').click();
// await page.waitForTimeout(2000);
await page.locator('button.checkout').click();
console.log('Click checkout');
// 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'
}).catch(_ => { });
await page.waitForSelector('#webPurchaseContainer iframe'); // TODO needed?
void (async () => {
try {
await page.locator('[name=accept-label]').check({ timeout: 10000 });
console.log('Accept End User License Agreement');
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');
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
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 {
// 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');
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.');
}).catch(_ => { }); // may time out if not shown
void (async () => {
try {
await captcha.waitFor({ timeout: 10000 });
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) {
db.data[user][id].status = 'claimed';
@ -176,16 +182,12 @@ try {
}
notify_games.forEach(g => g.status == 'failed' && (g.status = 'claimed'));
console.log('Claimed successfully!');
// context.setDefaultTimeout(cfg.timeout);
} catch (e) {
console.log(e);
// console.error(' Failed to claim! Try again if NopeCHA timed out. Click the extension to see if you ran out of credits (refill after 24h). To avoid captchas try to get a new IP or set a cookie from https://www.hcaptcha.com/accessibility');
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 });
// db.data[user][id].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...
console.log('Done');