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 () => {
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(_ => { });
page.waitForSelector('p:has-text("Incorrect response.")').then(async () => {
} catch {}
})();
void (async () => {
try {
await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 });
console.error('Incorrect response for captcha!');
}).catch(_ => { });
} 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 () => {
void (async () => {
try {
await error.waitFor({ timeout: 15000 });
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 () => {
} 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 ...');
// 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(_ => { });
} 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 () => {
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)');
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('input#agree').check();
await page.locator('button:has-text("Accept")').click();
}).catch(_ => { });
} 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 () => {
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(_ => { });
} 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.')
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 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 () => {
} 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' }); // TODO Bundle: got stuck here, but normal game now as well
} 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!');

25
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 () => {
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); // TODO still needed with wait for username below?
}).catch(_ => { });
iframe.locator('text=Invalid captcha').waitFor().then(() => {
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(_ => { });
} 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;
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;
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(() => {
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(_ => { });
// handle MFA, but don't await it
page.waitForURL('**/id/login/mfa**').then(async () => {
} 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 ...');
// 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(_ => { });
} 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(() => {
void (async () => {
try {
await page.locator('[name=accept-label]').check({ timeout: 10000 });
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?
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.')
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
} 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');