Merge branch 'main' into main

This commit is contained in:
Ralf Vogler 2024-06-25 13:30:42 +02:00 committed by GitHub
commit afceb0aad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 451 additions and 311 deletions

View file

@ -104,7 +104,7 @@ You can pass variables using `-e VAR=VAL`, for example `docker run -e EMAIL=foo@
If you are using [docker compose](https://docs.docker.com/compose/environment-variables/) (or Portainer etc.), you can put options in the `environment:` section. If you are using [docker compose](https://docs.docker.com/compose/environment-variables/) (or Portainer etc.), you can put options in the `environment:` section.
##### Without Docker ##### Without Docker
On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email. On Linux/macOS you can prefix the variables you want to set, for example `EMAIL=foo@bar.baz SHOW=1 node epic-games` will show the browser and skip asking you for your login email. On Windows you have to use `set`, [example](https://github.com/vogler/free-games-claimer/issues/314).
You can also put options in `data/config.env` which will be loaded by [dotenv](https://github.com/motdotla/dotenv). You can also put options in `data/config.env` which will be loaded by [dotenv](https://github.com/motdotla/dotenv).
### Notifications ### Notifications
@ -157,7 +157,7 @@ If you want it to run regularly, you have to schedule the runs yourself:
- Linux/macOS: `crontab -e` ([example](https://github.com/vogler/free-games-claimer/discussions/56)) - Linux/macOS: `crontab -e` ([example](https://github.com/vogler/free-games-claimer/discussions/56))
- macOS: [launchd](https://stackoverflow.com/questions/132955/how-do-i-set-a-task-to-run-every-so-often) - macOS: [launchd](https://stackoverflow.com/questions/132955/how-do-i-set-a-task-to-run-every-so-often)
- Windows: [task scheduler](https://active-directory-wp.com/docs/Usage/How_to_add_a_cron_job_on_Windows/Scheduled_tasks_and_cron_jobs_on_Windows/index.html), [other options](https://stackoverflow.com/questions/132971/what-is-the-windows-version-of-cron), or just put the command in a `.bat` file in Autostart if you restart often... - Windows: [task scheduler](https://active-directory-wp.com/docs/Usage/How_to_add_a_cron_job_on_Windows/Scheduled_tasks_and_cron_jobs_on_Windows/index.html) ([example](https://github.com/vogler/free-games-claimer/wiki/%5BHowTo%5D-Schedule-runs-on-Windows)), [other options](https://stackoverflow.com/questions/132971/what-is-the-windows-version-of-cron), or just put the command in a `.bat` file in Autostart if you restart often...
- any OS: use a process manager like [pm2](https://pm2.keymetrics.io/docs/usage/restart-strategies/) - any OS: use a process manager like [pm2](https://pm2.keymetrics.io/docs/usage/restart-strategies/)
- Docker Compose `command: bash -c "node epic-games; node prime-gaming; node gog; echo sleeping; sleep 1d"` additionally add `restart: unless-stopped` to it. - Docker Compose `command: bash -c "node epic-games; node prime-gaming; node gog; echo sleeping; sleep 1d"` additionally add `restart: unless-stopped` to it.

View file

@ -185,6 +185,8 @@ try {
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")');
urls.push(baseUrl); // add base game to the list of games to claim
urls.push(url); // add add-on itself again
} else { // GET } else { // GET
console.log(' Not in library yet! Click GET.'); console.log(' Not in library yet! Click GET.');
await page.click('[data-testid="purchase-cta-button"]', { delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough await page.click('[data-testid="purchase-cta-button"]', { delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
@ -249,6 +251,10 @@ try {
// console.info(' Saved a screenshot of hcaptcha challenge to', 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? // 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 }).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.');
});
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here
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

12
gog.js
View file

@ -10,6 +10,11 @@ console.log(datetime(), 'started checking gog');
const db = await jsonDb('gog.json', {}); const db = await jsonDb('gog.json', {});
if (cfg.width < 1280) { // otherwise 'Sign in' and #menuUsername are hidden (but attached to DOM), see https://github.com/vogler/free-games-claimer/issues/335
console.error(`Window width is set to ${cfg.width} but needs to be at least 1280 for GOG!`);
process.exit(1);
}
// https://playwright.dev/docs/auth#multi-factor-authentication // https://playwright.dev/docs/auth#multi-factor-authentication
const context = await firefox.launchPersistentContext(cfg.dir.browser, { const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless, headless: cfg.headless,
@ -93,10 +98,9 @@ try {
if (!await banner.count()) { if (!await banner.count()) {
console.log('Currently no free giveaway!'); console.log('Currently no free giveaway!');
} else { } else {
const text = await page.locator('.giveaway-banner__title').innerText(); const text = await page.locator('.giveaway__content-header').innerText();
const title = text.match(/Claim (.*)/)[1]; const title = text.match(/Claim (.*) and don't miss the/)[1];
const slug = await banner.getAttribute('href'); const url = await banner.locator('a').first().getAttribute('href');
const url = `https://gog.com${slug}`;
console.log(`Current free game: ${title} - ${url}`); console.log(`Current free game: ${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);

653
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,20 +17,20 @@
}, },
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=15" "node": ">=17"
}, },
"dependencies": { "dependencies": {
"chalk": "^5.3.0", "chalk": "^5.3.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"enquirer": "^2.4.1", "enquirer": "^2.4.1",
"lowdb": "^6.1.1", "lowdb": "^7.0.1",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"playwright-firefox": "^1.40.1", "playwright-firefox": "^1.45.0",
"puppeteer-extra-plugin-stealth": "^2.11.2" "puppeteer-extra-plugin-stealth": "^2.11.2"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin-js": "^1.5.1", "@stylistic/eslint-plugin-js": "^2.2.2",
"eslint": "^8.56.0" "eslint": "^9.5.0"
} }
} }

View file

@ -107,6 +107,26 @@ try {
// can't use .all() since the list of elements via locator will change after click while we iterate over it // can't use .all() since the list of elements via locator will change after click while we iterate over it
const internal = await games.locator('.item-card__action:has([data-a-target="FGWPOffer"])').elementHandles(); const internal = await games.locator('.item-card__action:has([data-a-target="FGWPOffer"])').elementHandles();
const external = await games.locator('.item-card__action:has([data-a-target="ExternalOfferClaim"])').all(); const external = await games.locator('.item-card__action:has([data-a-target="ExternalOfferClaim"])').all();
// bottom to top: oldest to newest games
internal.reverse();
external.reverse();
const checkTimeLeft = async url => {
// console.log(' Checking time left for game:', url);
const check = async p => {
console.log(' ', await p.locator('.availability-date').innerText());
const dueDateOrg = await p.locator('.availability-date .tw-bold').innerText();
const dueDate = datetime(new Date(Date.parse(dueDateOrg + ' 17:00')));
console.log(' Due date:', dueDate);
};
if (page.url() == url) {
await check(page);
} else {
const p = await context.newPage();
await p.goto(url, { waitUntil: 'domcontentloaded' });
await check(p);
p.close();
}
};
console.log('Number of free unclaimed games (Prime Gaming):', internal.length); console.log('Number of free unclaimed games (Prime Gaming):', internal.length);
// claim games in internal store // claim games in internal store
for (const card of internal) { for (const card of internal) {
@ -115,6 +135,7 @@ try {
const slug = await (await card.$('a')).getAttribute('href'); const slug = await (await card.$('a')).getAttribute('href');
const url = 'https://gaming.amazon.com' + slug.split('?')[0]; const url = 'https://gaming.amazon.com' + slug.split('?')[0];
console.log('Current free game:', title); console.log('Current free game:', title);
if (cfg.pg_timeLeft) await checkTimeLeft(url);
if (cfg.dryrun) continue; if (cfg.dryrun) continue;
if (cfg.interactive && !await confirm()) continue; if (cfg.interactive && !await confirm()) continue;
await (await card.$('.tw-button:has-text("Claim")')).click(); await (await card.$('.tw-button:has-text("Claim")')).click();
@ -134,6 +155,7 @@ try {
// await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait // await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait
external_info.push({ title, url }); external_info.push({ title, url });
} }
// external_info = [ { title: 'Fallout 76 (XBOX)', url: 'https://gaming.amazon.com/fallout-76-xbox-fgwp/dp/amzn1.pg.item.9fe17d7b-b6c2-4f58-b494-cc4e79528d0b?ingress=amzn&ref_=SM_Fallout76XBOX_S01_FGWP_CRWN' } ];
for (const { title, url } of external_info) { for (const { title, url } of external_info) {
console.log('Current free game:', title); // , url); console.log('Current free game:', title); // , url);
await page.goto(url, { waitUntil: 'domcontentloaded' }); await page.goto(url, { waitUntil: 'domcontentloaded' });
@ -141,6 +163,7 @@ try {
const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]'); const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]');
const store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1); const store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1);
console.log(' External store:', store); console.log(' External store:', store);
if (cfg.pg_timeLeft) await checkTimeLeft(url);
if (cfg.dryrun) continue; if (cfg.dryrun) continue;
if (cfg.interactive && !await confirm()) continue; if (cfg.interactive && !await confirm()) continue;
await Promise.any([page.click('[data-a-target="buy-box"] .tw-button:has-text("Get game")'), page.click('[data-a-target="buy-box"] .tw-button:has-text("Claim")'), page.click('.tw-button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")'), page.waitForSelector('.thank-you-title:has-text("Success")')]); // waits for navigation await Promise.any([page.click('[data-a-target="buy-box"] .tw-button:has-text("Get game")'), page.click('[data-a-target="buy-box"] .tw-button:has-text("Claim")'), page.click('.tw-button:has-text("Complete Claim")'), page.waitForSelector('div:has-text("Link game account")'), page.waitForSelector('.thank-you-title:has-text("Success")')]); // waits for navigation
@ -163,7 +186,8 @@ try {
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', // TODO still needed or now only via account linking?
'gog.com': 'https://www.gog.com/redeem', 'gog.com': 'https://www.gog.com/redeem',
'microsoft games': 'https://redeem.microsoft.com', 'microsoft store': 'https://account.microsoft.com/billing/redeem',
xbox: 'https://account.microsoft.com/billing/redeem',
'legacy games': 'https://www.legacygames.com/primedeal', 'legacy games': 'https://www.legacygames.com/primedeal',
}; };
if (store in redeem) { // did not work for linked origin: && !await page.locator('div:has-text("Successfully Claimed")').count() if (store in redeem) { // did not work for linked origin: && !await page.locator('div:has-text("Successfully Claimed")').count()
@ -218,27 +242,44 @@ try {
console.log(' Unknown Response 2 - please report in https://github.com/vogler/free-games-claimer/issues/5'); console.log(' Unknown Response 2 - please report in https://github.com/vogler/free-games-claimer/issues/5');
} }
} }
} else if (store == 'microsoft games') { } else if (store == 'microsoft store' || store == 'xbox') {
console.error(` Redeem on ${store} not yet implemented!`); console.error(` Redeem on ${store} is experimental!`);
// await page2.pause();
if (page2.url().startsWith('https://login.')) { if (page2.url().startsWith('https://login.')) {
console.error(' Not logged in! Use the browser to login manually.'); console.error(' Not logged in! Use the browser to login manually. Waiting for 60s.');
await page2.waitForTimeout(60 * 1000);
redeem_action = 'redeem (login)'; redeem_action = 'redeem (login)';
} else { } else {
const r = page2.waitForResponse(r => r.url().startsWith('https://purchase.mp.microsoft.com/')); const iframe = page2.frameLocator('#redeem-iframe');
await page2.fill('[name=tokenString]', code); const input = iframe.locator('[name=tokenString]');
await input.waitFor();
await input.fill(code);
const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/PrepareRedeem'));
// console.log(await page2.locator('.redeem_code_error').innerText()); // console.log(await page2.locator('.redeem_code_error').innerText());
const rt = await (await r).text(); const rt = await (await r).text();
console.debug(` Response: ${rt}`);
// {"code":"NotFound","data":[],"details":[],"innererror":{"code":"TokenNotFound",... // {"code":"NotFound","data":[],"details":[],"innererror":{"code":"TokenNotFound",...
const reason = JSON.parse(rt).code; const j = JSON.parse(rt);
if (reason == 'NotFound') { const reason = j?.events?.cart.length && j.events.cart[0]?.data?.reason;
if (reason == 'TokenNotFound') {
redeem_action = 'redeem (not found)'; redeem_action = 'redeem (not found)';
console.error(' Code was not found!'); console.error(' Code was not found!');
} else if (j?.productInfos?.length && j.productInfos[0]?.redeemable) {
await iframe.locator('button:has-text("Next")').click();
await iframe.locator('button:has-text("Confirm")').click();
const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/RedeemToken'));
const j = JSON.parse(await (await r).text());
if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') {
redeem_action = 'already redeemed';
console.error(' error: UserAlreadyOwnsContent');
} else if (true) { // TODO what's returned on success?
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 { // TODO find out other responses
await page2.click('#nextButton'); redeem_action = 'unknown';
redeem_action = 'redeemed?'; 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');
db.data[user][title].status = 'claimed and redeemed?';
} }
} }
} else if (store == 'legacy games') { } else if (store == 'legacy games') {

View file

@ -46,9 +46,9 @@ export const cfg = {
xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL, xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL,
xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD, xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD,
xbox_otpkey: process.env.XBOX_OTPKEY, xbox_otpkey: process.env.XBOX_OTPKEY,
// experimmental - likely to change // experimmental
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
lg_email: process.env.LG_EMAIL || process.env.PG_EMAIL || process.env.EMAIL, // prime-gaming: external: legacy-games: email to use for redeeming
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
// external stores pg_timeLeft: process.env.PG_TIMELEFT == '1', // prime-gaming: list time left to claim
lg_email: process.env.LG_EMAIL || process.env.PG_EMAIL || process.env.EMAIL, // legacy-games: email to use for redeeming
}; };

View file

@ -11,8 +11,8 @@ export const dataDir = s => path.resolve(__dirname, '..', 'data', s);
export const resolve = (...a) => a.length && a[0] == '0' ? null : path.resolve(...a); export const resolve = (...a) => a.length && a[0] == '0' ? null : path.resolve(...a);
// json database // json database
import { JSONPreset } from 'lowdb/node'; import { JSONFilePreset } from 'lowdb/node';
export const jsonDb = (file, defaultData) => JSONPreset(dataDir(file), defaultData); export const jsonDb = (file, defaultData) => JSONFilePreset(dataDir(file), defaultData);
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// date and time as UTC (no timezone offset) in nicely readable and sortable format, e.g., 2022-10-06 12:05:27.313 // date and time as UTC (no timezone offset) in nicely readable and sortable format, e.g., 2022-10-06 12:05:27.313