From 61fdf566c9c2748ce06e35cd7262d314151c836d Mon Sep 17 00:00:00 2001 From: Rex Date: Fri, 1 Mar 2024 20:57:00 +0800 Subject: [PATCH 01/33] Resolve error with no default data --- xbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xbox.js b/xbox.js index b2406cc..67054ef 100644 --- a/xbox.js +++ b/xbox.js @@ -15,7 +15,7 @@ const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold"; console.log(datetime(), 'started checking xbox'); -const db = await jsonDb('xbox.json'); +const db = await jsonDb('xbox.json', {}); db.data ||= {}; handleSIGINT(); From da6964c19afbb24848c997b1d8b1f9995d360791 Mon Sep 17 00:00:00 2001 From: Targunitoth Date: Thu, 25 Apr 2024 13:51:40 +0200 Subject: [PATCH 02/33] Escape the apprise -b parameter --- src/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index bda3f78..69cbf7a 100644 --- a/src/util.js +++ b/src/util.js @@ -116,7 +116,7 @@ export const notify = html => new Promise((resolve, reject) => { return resolve(); } // const cmd = `apprise '${cfg.notify}' ${title} -i html -b '${html}'`; // this had problems if e.g. ' was used in arg; could have `npm i shell-escape`, but instead using safer execFile which takes args as array instead of exec which spawned a shell to execute the command - const args = [cfg.notify, '-i', 'html', '-b', html]; + const args = [cfg.notify, '-i', 'html', '-b', `'${html}'`]; if (cfg.notify_title) args.push(...['-t', cfg.notify_title]); if (cfg.debug) console.debug(`apprise ${args.map(a => `'${a}'`).join(' ')}`); // this also doesn't escape, but it's just for info execFile('apprise', args, (error, stdout, stderr) => { From 3338d08a1d29584d7a209656b17266cd19b06c7a Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 2 May 2024 16:58:31 +0200 Subject: [PATCH 03/33] eg: fix title for Bundles, TODO got stuck after happened for 'LISA: The Definitive Edition' - https://store.epicgames.com/en-US/bundles/lisa-the-definitive-edition --- epic-games.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/epic-games.js b/epic-games.js index 68a2dde..b703de5 100644 --- a/epic-games.js +++ b/epic-games.js @@ -159,7 +159,13 @@ try { await page.waitForTimeout(2000); } - const title = await page.locator('h1').first().innerText(); + let title; + if (await page.locator('span:text-is("About Bundle")').count()) { + // console.log(' This is a bundle containing: TODO'); + title = (await page.locator('span:has-text("Buy"):left-of([data-testid="purchase-cta-button"])').first().innerText()).replace('Buy ', ''); + } else { + title = await page.locator('h1').first().innerText(); + } const game_id = page.url().split('/').pop(); db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! console.log('Current free game:', title); @@ -191,8 +197,8 @@ try { // Accept End User License Agreement (only needed once) page.locator('input#agree').waitFor().then(async () => { - console.log('Accept End User License Agreement (only needed once)'); - await page.locator('input#agree').check(); + console.log(' Accept End User License Agreement (only needed once)'); + await page.locator('input#agree').check(); // TODO Bundle: got stuck here await page.locator('button:has-text("Accept")').click(); }).catch(_ => { }); @@ -243,7 +249,7 @@ try { // console.info(' Saved a screenshot of hcaptcha challenge to', p); // console.error(' Got hcaptcha challenge. To avoid it, get a link from https://www.hcaptcha.com/accessibility'); // TODO save this link in config and visit it daily to set accessibility cookie to avoid captcha challenge? }).catch(_ => { }); // may time out if not shown - await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); + await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' }); // TODO Bundle: got stuck here db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time console.log(' Claimed successfully!'); From 075106da11ce4bb250a11a47ba232072e9546f1a Mon Sep 17 00:00:00 2001 From: Tymec Date: Sat, 18 May 2024 19:45:59 +0200 Subject: [PATCH 04/33] Update config.js --- src/config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config.js b/src/config.js index bd41c7d..4c32d59 100644 --- a/src/config.js +++ b/src/config.js @@ -49,4 +49,6 @@ export const cfg = { // experimmental - likely to change pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content + // external stores + lg_email: process.env.LG_EMAIL || process.env.PG_EMAIL || process.env.EMAIL, // legacy-games: email to use for redeeming }; From 8c535f48fa8f2de63fe32cf4a000b962c3a60e6b Mon Sep 17 00:00:00 2001 From: Tymec Date: Sat, 18 May 2024 19:47:18 +0200 Subject: [PATCH 05/33] Update prime-gaming.js --- prime-gaming.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prime-gaming.js b/prime-gaming.js index b555f67..2bd705f 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -243,8 +243,8 @@ try { } } else if (store == 'legacy games') { await page2.fill('[name=coupon_code]', code); - await page2.fill('[name=email]', cfg.pg_email); // TODO option for sep. email? - await page2.fill('[name=email_validate]', cfg.pg_email); + await page2.fill('[name=email]', cfg.lg_email); + await page2.fill('[name=email_validate]', cfg.lg_email); await page2.uncheck('[name=newsletter_sub]'); await page2.click('[type="submit"]'); try { From c4ae3505b38f25361bf1a16ab2089fffc63e4d9f Mon Sep 17 00:00:00 2001 From: Tymec Date: Sat, 18 May 2024 19:50:13 +0200 Subject: [PATCH 06/33] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cfdddec..1491257 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Available options/variables and their default values: | GOG_EMAIL | | GOG email for login. Overrides EMAIL. | | GOG_PASSWORD | | GOG password for login. Overrides PASSWORD. | | GOG_NEWSLETTER | 0 | Do not unsubscribe from newsletter after claiming a game if 1. | +| LG_EMAIL | | Legacy Games: email to use for redeeming (if not set, defaults to PG_EMAIL) | See `src/config.js` for all options. From e49fa930e0b426a497ca32e0d30cbfe707ece459 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 23 May 2024 15:54:23 +0200 Subject: [PATCH 07/33] eg: claim base game before add-on --- epic-games.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/epic-games.js b/epic-games.js index b703de5..3ead561 100644 --- a/epic-games.js +++ b/epic-games.js @@ -185,6 +185,8 @@ try { 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")'); + 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 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 From 9ef9798626c7f6108b0eb76cb651f8027f7a6931 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 23 May 2024 15:55:07 +0200 Subject: [PATCH 08/33] eg: detect 'Failed to challenge captcha, please try again later.' --- epic-games.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/epic-games.js b/epic-games.js index 3ead561..54ca597 100644 --- a/epic-games.js +++ b/epic-games.js @@ -251,6 +251,10 @@ try { // console.info(' Saved a screenshot of hcaptcha challenge to', p); // console.error(' Got hcaptcha challenge. To avoid it, get a link from https://www.hcaptcha.com/accessibility'); // TODO save this link in config and visit it daily to set accessibility cookie to avoid captcha challenge? }).catch(_ => { }); // may time out if not shown + 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 db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time From f7c23569c7a40fa6f56d5a32d855e0b60c530a0e Mon Sep 17 00:00:00 2001 From: Vladimir Budylnikov Date: Sun, 2 Jun 2024 13:33:51 +0400 Subject: [PATCH 09/33] Update README.md I've made i howto for windows users --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfdddec..a6e3825 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,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)) - 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/) - Docker Compose `command: bash -c "node epic-games; node prime-gaming; node gog; echo sleeping; sleep 1d"` additionally add `restart: unless-stopped` to it. From cf59da5d9e74d49d684762625ae2446341115336 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 18:02:02 +0200 Subject: [PATCH 10/33] gog: new locators, fixes #326, #334 --- gog.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gog.js b/gog.js index 651afa6..de5d02f 100644 --- a/gog.js +++ b/gog.js @@ -93,10 +93,9 @@ try { if (!await banner.count()) { console.log('Currently no free giveaway!'); } else { - const text = await page.locator('.giveaway-banner__title').innerText(); - const title = text.match(/Claim (.*)/)[1]; - const slug = await banner.getAttribute('href'); - const url = `https://gog.com${slug}`; + const text = await page.locator('.giveaway__content-header').innerText(); + const title = text.match(/Claim (.*) and don't miss the/)[1]; + const url = await banner.locator('a').first().getAttribute('href'); console.log(`Current free game: ${title} - ${url}`); db.data[user][title] ||= { title, time: datetime(), url }; if (cfg.dryrun) process.exit(1); From f0f142733e0fd7e365a239fbc8d737a70a739cbc Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 19:14:13 +0200 Subject: [PATCH 11/33] ncu -u: lowdb 6 -> 7 dropped support for Node 16 --- package-lock.json | 647 ++++++++++++++++++++++++++-------------------- package.json | 12 +- src/util.js | 4 +- 3 files changed, 376 insertions(+), 287 deletions(-) diff --git a/package-lock.json b/package-lock.json index 930e7bf..3d1d878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,16 @@ "dependencies": { "chalk": "^5.3.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", - "lowdb": "^6.1.1", + "lowdb": "^7.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.40.1", + "playwright-firefox": "^1.44.1", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "devDependencies": { - "@stylistic/eslint-plugin-js": "^1.5.1", - "eslint": "^8.56.0" + "@stylistic/eslint-plugin-js": "^2.2.2", + "eslint": "^9.5.0" }, "engines": { "node": ">=15" @@ -59,16 +59,32 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", + "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -76,33 +92,30 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -118,11 +131,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -202,23 +223,37 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.1.tgz", - "integrity": "sha512-iZF0rF+uOhAmOJYOJx1Yvmm3CZ1uz9n0SRd9dpBYHA3QAvfABUORh9LADWwZCigjHJkp2QbCZelGFJGwGz7Siw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.2.2.tgz", + "integrity": "sha512-Vj2Q1YHVvJw+ThtOvmk5Yx7wZanVrIBRUTT89horLDb4xdP9GA1um9XOYQC6j67VeUC2gjZQnz5/RVJMzaOhtw==", "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.11.2", - "escape-string-regexp": "^4.0.0", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1" + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { "eslint": ">=8.40.0" } }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@types/debug": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", @@ -227,22 +262,42 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -255,6 +310,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -264,6 +320,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -310,7 +367,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/arr-union": { "version": "3.1.0", @@ -339,6 +397,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -452,27 +511,16 @@ "node": ">=0.10.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/enquirer": { @@ -500,41 +548,38 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", + "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/config-array": "^0.16.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.5.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -548,23 +593,24 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -598,18 +644,45 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -632,6 +705,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -653,6 +727,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -661,13 +736,15 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -685,15 +762,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/find-up": { @@ -713,24 +791,25 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" }, "node_modules/for-in": { "version": "1.0.2", @@ -801,15 +880,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -820,12 +897,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -836,10 +907,11 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -849,6 +921,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -955,6 +1028,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -966,13 +1040,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -996,6 +1072,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -1054,14 +1131,15 @@ "dev": true }, "node_modules/lowdb": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-6.1.1.tgz", - "integrity": "sha512-HO13FCxI8SCwfj2JRXOKgXggxnmfSc+l0aJsZ5I34X3pwzG/DPBSKyKu3Zkgg/pNmx854SVgE2la0oUeh6wzNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "license": "MIT", "dependencies": { - "steno": "^3.1.1" + "steno": "^4.0.2" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -1192,6 +1270,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -1225,9 +1304,10 @@ } }, "node_modules/playwright-core": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", - "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -1236,12 +1316,13 @@ } }, "node_modules/playwright-firefox": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.40.1.tgz", - "integrity": "sha512-+C5eOWZv/CvALM0yBZFSYThvUzGKusNw6soDMhTEJwDUB5i9q/yZVFkj6I8CFXM4U6Pf1q0PXHMca70HoIwnCQ==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.44.1.tgz", + "integrity": "sha512-ywwHQGTLM7P5r3SzVTSyRQQUK8xsCj6MrIqY9cn8SNz+GkKL4atZb1KuYDulxrfKFzZWXLJ8M+VGc0/vNWLMfA==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.40.1" + "playwright-core": "1.44.1" }, "bin": { "playwright": "cli.js" @@ -1264,6 +1345,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1395,6 +1477,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -1499,11 +1582,12 @@ } }, "node_modules/steno": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/steno/-/steno-3.1.1.tgz", - "integrity": "sha512-B7c6EVH7oEiaMRW36SjUnktkDwp/qd4pQiduylyiqvcZEZDeX0IIFZRBZdwO/RaVo60M0wkDwC0e8yeKaR4VGg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -1525,6 +1609,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -1570,18 +1655,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -1595,6 +1668,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -1653,16 +1727,27 @@ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true }, + "@eslint/config-array": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", + "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1671,21 +1756,16 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", "dev": true }, - "@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, "@humanwhocodes/module-importer": { "version": "1.0.1", @@ -1693,10 +1773,10 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true }, "@nodelib/fs.scandir": { @@ -1768,15 +1848,23 @@ } }, "@stylistic/eslint-plugin-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.1.tgz", - "integrity": "sha512-iZF0rF+uOhAmOJYOJx1Yvmm3CZ1uz9n0SRd9dpBYHA3QAvfABUORh9LADWwZCigjHJkp2QbCZelGFJGwGz7Siw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.2.2.tgz", + "integrity": "sha512-Vj2Q1YHVvJw+ThtOvmk5Yx7wZanVrIBRUTT89horLDb4xdP9GA1um9XOYQC6j67VeUC2gjZQnz5/RVJMzaOhtw==", "dev": true, "requires": { - "acorn": "^8.11.2", - "escape-string-regexp": "^4.0.0", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1" + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true + } } }, "@types/debug": { @@ -1787,21 +1875,37 @@ "@types/ms": "*" } }, + "@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true }, "acorn-jsx": { @@ -1947,19 +2051,10 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, "dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, "enquirer": { "version": "2.4.1", @@ -1977,41 +2072,37 @@ "dev": true }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", + "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/config-array": "^0.16.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.5.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2031,13 +2122,19 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } + }, + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true } } }, "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -2051,14 +2148,22 @@ "dev": true }, "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true + } } }, "esquery": { @@ -2119,12 +2224,12 @@ } }, "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" } }, "find-up": { @@ -2138,20 +2243,19 @@ } }, "flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "requires": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" } }, "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "for-in": { @@ -2205,25 +2309,16 @@ } }, "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2231,9 +2326,9 @@ "dev": true }, "ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true }, "import-fresh": { @@ -2399,11 +2494,11 @@ "dev": true }, "lowdb": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-6.1.1.tgz", - "integrity": "sha512-HO13FCxI8SCwfj2JRXOKgXggxnmfSc+l0aJsZ5I34X3pwzG/DPBSKyKu3Zkgg/pNmx854SVgE2la0oUeh6wzNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", "requires": { - "steno": "^3.1.1" + "steno": "^4.0.2" } }, "merge-deep": { @@ -2527,16 +2622,16 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "playwright-core": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", - "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==" + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==" }, "playwright-firefox": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.40.1.tgz", - "integrity": "sha512-+C5eOWZv/CvALM0yBZFSYThvUzGKusNw6soDMhTEJwDUB5i9q/yZVFkj6I8CFXM4U6Pf1q0PXHMca70HoIwnCQ==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.44.1.tgz", + "integrity": "sha512-ywwHQGTLM7P5r3SzVTSyRQQUK8xsCj6MrIqY9cn8SNz+GkKL4atZb1KuYDulxrfKFzZWXLJ8M+VGc0/vNWLMfA==", "requires": { - "playwright-core": "1.40.1" + "playwright-core": "1.44.1" } }, "prelude-ls": { @@ -2668,9 +2763,9 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "steno": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/steno/-/steno-3.1.1.tgz", - "integrity": "sha512-B7c6EVH7oEiaMRW36SjUnktkDwp/qd4pQiduylyiqvcZEZDeX0IIFZRBZdwO/RaVo60M0wkDwC0e8yeKaR4VGg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==" }, "strip-ansi": { "version": "6.0.1", @@ -2715,12 +2810,6 @@ "prelude-ls": "^1.2.1" } }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 6c954cd..b2eee9f 100644 --- a/package.json +++ b/package.json @@ -17,20 +17,20 @@ }, "type": "module", "engines": { - "node": ">=15" + "node": ">=17" }, "dependencies": { "chalk": "^5.3.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", - "lowdb": "^6.1.1", + "lowdb": "^7.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.40.1", + "playwright-firefox": "^1.44.1", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "devDependencies": { - "@stylistic/eslint-plugin-js": "^1.5.1", - "eslint": "^8.56.0" + "@stylistic/eslint-plugin-js": "^2.2.2", + "eslint": "^9.5.0" } } diff --git a/src/util.js b/src/util.js index bda3f78..20c2f97 100644 --- a/src/util.js +++ b/src/util.js @@ -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); // json database -import { JSONPreset } from 'lowdb/node'; -export const jsonDb = (file, defaultData) => JSONPreset(dataDir(file), defaultData); +import { JSONFilePreset } from 'lowdb/node'; +export const jsonDb = (file, defaultData) => JSONFilePreset(dataDir(file), defaultData); 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 From 30b1835bdbf850eae389a0010945ca607cf61f6a Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 19:16:01 +0200 Subject: [PATCH 12/33] pg: reverse games: oldest/bottom to newest/top --- prime-gaming.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prime-gaming.js b/prime-gaming.js index b555f67..ca3fcee 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -107,6 +107,9 @@ try { // 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 external = await games.locator('.item-card__action:has([data-a-target="ExternalOfferClaim"])').all(); + // bottom to top: oldest to newest games + internal.reverse(); + external.reverse(); console.log('Number of free unclaimed games (Prime Gaming):', internal.length); // claim games in internal store for (const card of internal) { From f920aa26d0d89404a97a715c14f8f7b9400beda5 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 19:18:30 +0200 Subject: [PATCH 13/33] pg: checkTimeLeft via PG_TIMELEFT=1 --- prime-gaming.js | 19 +++++++++++++++++++ src/config.js | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/prime-gaming.js b/prime-gaming.js index ca3fcee..d8caaeb 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -110,6 +110,23 @@ try { // 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); // claim games in internal store for (const card of internal) { @@ -118,6 +135,7 @@ try { const slug = await (await card.$('a')).getAttribute('href'); const url = 'https://gaming.amazon.com' + slug.split('?')[0]; console.log('Current free game:', title); + if (cfg.pg_timeLeft) await checkTimeLeft(url); if (cfg.dryrun) continue; if (cfg.interactive && !await confirm()) continue; await (await card.$('.tw-button:has-text("Claim")')).click(); @@ -144,6 +162,7 @@ try { const item_text = await page.innerText('[data-a-target="DescriptionItemDetails"]'); const store = item_text.toLowerCase().replace(/.* on /, '').slice(0, -1); console.log(' External store:', store); + if (cfg.pg_timeLeft) await checkTimeLeft(url); if (cfg.dryrun) 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 diff --git a/src/config.js b/src/config.js index bd41c7d..56e42d2 100644 --- a/src/config.js +++ b/src/config.js @@ -46,7 +46,8 @@ export const cfg = { xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL, xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD, 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_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content + pg_timeLeft: process.env.PG_TIMELEFT == '1', // prime-gaming: list time left to claim }; From 43bef9b23c93104345fa2e9b52c1db6ef231bbba Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 20:44:03 +0200 Subject: [PATCH 14/33] pg: redeem microsoft store and xbox, WIP, #315, #5 --- prime-gaming.js | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/prime-gaming.js b/prime-gaming.js index d8caaeb..1f20a47 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -155,6 +155,7 @@ try { // await (await card.$('text=Claim')).click(); // goes to URL of game, no need to wait 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) { console.log('Current free game:', title); // , url); await page.goto(url, { waitUntil: 'domcontentloaded' }); @@ -185,7 +186,8 @@ try { const redeem = { // 'origin': 'https://www.origin.com/redeem', // TODO still needed or now only via account linking? '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', }; if (store in redeem) { // did not work for linked origin: && !await page.locator('div:has-text("Successfully Claimed")').count() @@ -240,27 +242,44 @@ try { console.log(' Unknown Response 2 - please report in https://github.com/vogler/free-games-claimer/issues/5'); } } - } else if (store == 'microsoft games') { - console.error(` Redeem on ${store} not yet implemented!`); + } else if (store == 'microsoft store' || store == 'xbox') { + console.error(` Redeem on ${store} is experimental!`); + // await page2.pause(); 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)'; } else { - const r = page2.waitForResponse(r => r.url().startsWith('https://purchase.mp.microsoft.com/')); - await page2.fill('[name=tokenString]', code); + const iframe = page2.frameLocator('#redeem-iframe'); + 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()); const rt = await (await r).text(); - console.debug(` Response: ${rt}`); // {"code":"NotFound","data":[],"details":[],"innererror":{"code":"TokenNotFound",... - const reason = JSON.parse(rt).code; - if (reason == 'NotFound') { + const j = JSON.parse(rt); + const reason = j?.events?.cart.length && j.events.cart[0]?.data?.reason; + if (reason == 'TokenNotFound') { redeem_action = 'redeem (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 - await page2.click('#nextButton'); - redeem_action = 'redeemed?'; + 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'); - db.data[user][title].status = 'claimed and redeemed?'; } } } else if (store == 'legacy games') { From 601e893714731767ca5ce7ef6ca46c1e3330ed60 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Sun, 23 Jun 2024 20:50:00 +0200 Subject: [PATCH 15/33] README: link example how to set env vars on Windows, closes #314 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6e3825..a2da668 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,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. ##### 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). ### Notifications From 076738e3012ff9f90b75e9c6161f63314c96d6a1 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 25 Jun 2024 13:09:10 +0200 Subject: [PATCH 16/33] gog: fail if WIDTH<1280 due to hidden username, closes #335 --- gog.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gog.js b/gog.js index de5d02f..40d8fad 100644 --- a/gog.js +++ b/gog.js @@ -10,6 +10,11 @@ console.log(datetime(), 'started checking gog'); 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 const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, From c0eb6dbb0bb6c64e268ae70b9e37586c92a59366 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 25 Jun 2024 13:24:55 +0200 Subject: [PATCH 17/33] ncu -u: playwright 1.44.1 -> 1.45.0 --- package-lock.json | 36 ++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d1d878..e6f66b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "enquirer": "^2.4.1", "lowdb": "^7.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.44.1", + "playwright-firefox": "^1.45.0", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "devDependencies": { @@ -23,7 +23,7 @@ "eslint": "^9.5.0" }, "engines": { - "node": ">=15" + "node": ">=17" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1304,31 +1304,31 @@ } }, "node_modules/playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz", + "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/playwright-firefox": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.44.1.tgz", - "integrity": "sha512-ywwHQGTLM7P5r3SzVTSyRQQUK8xsCj6MrIqY9cn8SNz+GkKL4atZb1KuYDulxrfKFzZWXLJ8M+VGc0/vNWLMfA==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.45.0.tgz", + "integrity": "sha512-JmGESfFR8xTjAYQzECYO00yBbSSnu4dBImsrmJVeOXTvT+i9p1dpVUaxKz6lTFMI/xzYROqB4E4Km8NBiOgslw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.44.1" + "playwright-core": "1.45.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/prelude-ls": { @@ -2622,16 +2622,16 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==" + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz", + "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==" }, "playwright-firefox": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.44.1.tgz", - "integrity": "sha512-ywwHQGTLM7P5r3SzVTSyRQQUK8xsCj6MrIqY9cn8SNz+GkKL4atZb1KuYDulxrfKFzZWXLJ8M+VGc0/vNWLMfA==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.45.0.tgz", + "integrity": "sha512-JmGESfFR8xTjAYQzECYO00yBbSSnu4dBImsrmJVeOXTvT+i9p1dpVUaxKz6lTFMI/xzYROqB4E4Km8NBiOgslw==", "requires": { - "playwright-core": "1.44.1" + "playwright-core": "1.45.0" } }, "prelude-ls": { diff --git a/package.json b/package.json index b2eee9f..2c64f46 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "enquirer": "^2.4.1", "lowdb": "^7.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.44.1", + "playwright-firefox": "^1.45.0", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "devDependencies": { From 55226933c0eb01a0f0b258732e1bb3fc67849200 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 25 Jun 2024 15:30:02 +0200 Subject: [PATCH 18/33] filenamify datetime recordHar for Windows : -> ., fix #336 --- epic-games.js | 2 +- gog.js | 2 +- prime-gaming.js | 2 +- unrealengine.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/epic-games.js b/epic-games.js index 54ca597..5487445 100644 --- a/epic-games.js +++ b/epic-games.js @@ -25,7 +25,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { // 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 recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 - recordHar: cfg.record ? { path: `data/record/eg-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + 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 diff --git a/gog.js b/gog.js index 40d8fad..0688626 100644 --- a/gog.js +++ b/gog.js @@ -21,7 +21,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { viewport: { width: cfg.width, height: cfg.height }, locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 - recordHar: cfg.record ? { path: `data/record/gog-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + recordHar: cfg.record ? { path: `data/record/gog-${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 }); diff --git a/prime-gaming.js b/prime-gaming.js index 6e8cb03..91d3fe2 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -19,7 +19,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { viewport: { width: cfg.width, height: cfg.height }, locale: 'en-US', // ignore OS locale to be sure to have english text for locators recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 - recordHar: cfg.record ? { path: `data/record/pg-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + recordHar: cfg.record ? { path: `data/record/pg-${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 }); diff --git a/unrealengine.js b/unrealengine.js index e67633e..2bb8ee9 100644 --- a/unrealengine.js +++ b/unrealengine.js @@ -25,7 +25,7 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { // 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 recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 - recordHar: cfg.record ? { path: `data/record/ue-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + 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 handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved }); From 9cf5d2f7f21157566dad636b75febb831d14b333 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 25 Jun 2024 15:50:02 +0200 Subject: [PATCH 19/33] pg: include code in redeem_url for gog, closes #330 --- prime-gaming.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prime-gaming.js b/prime-gaming.js index 91d3fe2..5e3e283 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -196,7 +196,9 @@ try { if (store == 'legacy games') { // may be different URL like https://legacygames.com/primeday/puzzleoftheyear/ redeem[store] = await (await page.$('li:has-text("Click here") a')).getAttribute('href'); // full text: Click here to enter your redemption code. } - console.log(' URL to redeem game:', redeem[store]); + let redeem_url = redeem[store]; + if (store == 'gog.com') redeem_url += '/' + code; // to log and notify, but can't use for goto below (captcha) + console.log(' URL to redeem game:', redeem_url); db.data[user][title].code = code; let redeem_action = 'redeem'; if (cfg.pg_redeem) { // try to redeem keys on external stores @@ -305,7 +307,7 @@ try { if (cfg.debug) await page2.pause(); await page2.close(); } - notify_game.status = `${redeem_action} ${code} on ${store}`; + notify_game.status = `${redeem_action} ${code} on ${store}`; } else { notify_game.status = `claimed on ${store}`; db.data[user][title].status = 'claimed'; From c4be7ece21702fb73bc7ed61bafd7abc6133a911 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Tue, 25 Jun 2024 18:45:21 +0200 Subject: [PATCH 20/33] xbox: fixup #307 --- xbox.js | 1 - 1 file changed, 1 deletion(-) diff --git a/xbox.js b/xbox.js index 67054ef..fd78c66 100644 --- a/xbox.js +++ b/xbox.js @@ -16,7 +16,6 @@ const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold"; console.log(datetime(), 'started checking xbox'); const db = await jsonDb('xbox.json', {}); -db.data ||= {}; handleSIGINT(); From 9af36e902c0634d8fc19fdb8a75267b0887ccf47 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Wed, 26 Jun 2024 17:10:04 +0200 Subject: [PATCH 21/33] eg: fix login: password now together with email again, detect login error, closes #338, closes #337 --- epic-games.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/epic-games.js b/epic-games.js index 5487445..70807b8 100644 --- a/epic-games.js +++ b/epic-games.js @@ -20,7 +20,7 @@ if (cfg.time) console.time('startup'); 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: '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 locale: 'en-US', // ignore OS locale to be sure to have english text for locators @@ -90,8 +90,6 @@ try { if (!email) await notifyBrowserLogin(); else { // await page.click('text=Sign in with Epic Games'); - await page.fill('#email', email); - await page.click('button[type="submit"]'); 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.'); @@ -99,10 +97,20 @@ try { page.waitForSelector('p:has-text("Incorrect response.")').then(async () => { console.error('Incorrect repsonse 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(); - await page.fill('#password', password); - await page.click('button[type="submit"]'); + else { + await page.fill('#password', password); + await page.click('button[type="submit"]'); + } + const error = page.locator('#form-error-message'); + error.waitFor().then(async () => { + console.error('Login error:', await error.innerText()); + await context.close(); // finishes potential recording + process.exit(1); + }).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 ...'); From d85cd8d20ccb27834bfaafd98f8be95fecb7a3d5 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Jun 2024 10:54:46 +0200 Subject: [PATCH 22/33] rm xbox.js, no more games with gold, closes #286, closes #339 TODO integrate login code for microsoft games in prime-gaming --- src/config.js | 4 - xbox.js | 250 -------------------------------------------------- 2 files changed, 254 deletions(-) delete mode 100644 xbox.js diff --git a/src/config.js b/src/config.js index 9624471..03414a0 100644 --- a/src/config.js +++ b/src/config.js @@ -42,10 +42,6 @@ export const cfg = { gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD, gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game // OTP only via GOG_EMAIL, can't add app... - // auth xbox - xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL, - xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD, - xbox_otpkey: process.env.XBOX_OTPKEY, // experimmental 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 diff --git a/xbox.js b/xbox.js deleted file mode 100644 index fd78c66..0000000 --- a/xbox.js +++ /dev/null @@ -1,250 +0,0 @@ -import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra -import { authenticator } from 'otplib'; -import { - datetime, - handleSIGINT, - html_game_list, - jsonDb, - notify, - prompt, -} from './src/util.js'; -import { cfg } from './src/config.js'; - -// ### SETUP -const URL_CLAIM = 'https://www.xbox.com/en-US/live/gold'; // #gameswithgold"; - -console.log(datetime(), 'started checking xbox'); - -const db = await jsonDb('xbox.json', {}); - -handleSIGINT(); - -// https://playwright.dev/docs/auth#multi-factor-authentication -const context = await firefox.launchPersistentContext(cfg.dir.browser, { - headless: cfg.headless, - viewport: { width: cfg.width, height: cfg.height }, - locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL -}); - -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 - -const notify_games = []; -let user; - -main(); - -async function main() { - try { - await performLogin(); - await getAndSaveUser(); - await redeemFreeGames(); - } catch (error) { - console.error(error); - process.exitCode ||= 1; - if (error.message && process.exitCode != 130) notify(`xbox failed: ${error.message.split('\n')[0]}`); - } finally { - await db.write(); // write out json db - if (notify_games.filter(g => g.status != 'existed').length) { - // don't notify if all were already claimed - notify(`xbox (${user}):
${html_game_list(notify_games)}`); - } - await context.close(); - } -} - -async function performLogin() { - await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever - - const signInLocator = page - .getByRole('link', { - name: 'Sign in to your account', - }) - .first(); - const usernameLocator = page - .getByRole('button', { - name: 'Account manager for', - }) - .first(); - - await Promise.any([signInLocator.waitFor(), usernameLocator.waitFor()]); - - if (await usernameLocator.isVisible()) { - return; // logged in using saved cookie - } else if (await signInLocator.isVisible()) { - console.error('Not signed in anymore.'); - await signInLocator.click(); - await signInToXbox(); - } else { - console.error('lost! where am i?'); - } -} - -async function signInToXbox() { - page.waitForLoadState('domcontentloaded'); - if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in - console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`); - - // ### FETCH EMAIL/PASS - if (cfg.xbox_email && cfg.xbox_password) console.info('Using email and password from environment.'); - else console.info( - 'Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).', - ); - const email = cfg.xbox_email || await prompt({ message: 'Enter email' }); - const password = - email && - (cfg.xbox_password || - await prompt({ - type: 'password', - message: 'Enter password', - })); - // ### FILL IN EMAIL/PASS - if (email && password) { - const usernameLocator = page - .getByPlaceholder('Email, phone, or Skype') - .first(); - const passwordLocator = page.getByPlaceholder('Password').first(); - - await Promise.any([ - usernameLocator.waitFor(), - passwordLocator.waitFor(), - ]); - - // username may already be saved from before, if so, skip to filling in password - if (await page.getByPlaceholder('Email, phone, or Skype').isVisible()) { - await usernameLocator.fill(email); - await page.getByRole('button', { name: 'Next' }).click(); - } - - await passwordLocator.fill(password); - await page.getByRole('button', { name: 'Sign in' }).click(); - - // handle MFA, but don't await it - page.locator('input[name="otc"]') - .waitFor() - .then(async () => { - console.log('Two-Step Verification - Enter security code'); - console.log( - await page - .locator('div[data-bind="text: description"]') - .innerText(), - ); - const otp = - cfg.xbox_otpkey && - authenticator.generate(cfg.xbox_otpkey) || - await prompt({ - type: 'text', - message: 'Enter two-factor sign in code', - validate: n => n.toString().length == 6 || - 'The code must be 6 digits!', - }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await page.type('input[name="otc"]', otp.toString()); - await page - .getByLabel('Don\'t ask me again on this device') - .check(); // Trust this Browser - await page.getByRole('button', { name: 'Verify' }).click(); - }) - .catch(_ => {}); - - // Trust this browser, but don't await it - page.getByLabel('Don\'t show this again') - .waitFor() - .then(async () => { - await page.getByLabel('Don\'t show this again').check(); - await page.getByRole('button', { name: 'Yes' }).click(); - }) - .catch(_ => {}); - } else { - console.log('Waiting for you to login in the browser.'); - await notify( - 'xbox: no longer signed in and not enough options set for automatic login.', - ); - if (cfg.headless) { - console.log( - 'Run `SHOW=1 node xbox` to login in the opened browser.', - ); - await context.close(); - process.exit(1); - } - } - - // ### VERIFY SIGNED IN - await page.waitForURL(`${URL_CLAIM}**`); - - if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); -} - -async function getAndSaveUser() { - user = await page.locator('#mectrl_currentAccount_primary').innerHTML(); - console.log(`Signed in as '${user}'`); - db.data[user] ||= {}; -} - -async function redeemFreeGames() { - const monthlyGamesLocator = await page.locator('.f-size-large').all(); - - const monthlyGamesPageLinks = await Promise.all( - monthlyGamesLocator.map( - async el => await el.locator('a').getAttribute('href'), - ), - ); - console.log('Free games:', monthlyGamesPageLinks); - - for (const url of monthlyGamesPageLinks) { - await page.goto(url); - - const title = await page.locator('h1').first().innerText(); - const game_id = page.url().split('/').pop(); - db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! - console.log('Current free game:', title); - const notify_game = { title, url, status: 'failed' }; - notify_games.push(notify_game); // status is updated below - - // SELECTORS - const getBtnLocator = page.getByText('GET', { exact: true }).first(); - const installToLocator = page - .getByText('INSTALL TO', { exact: true }) - .first(); - - await Promise.any([ - getBtnLocator.waitFor(), - installToLocator.waitFor(), - ]); - - if (await installToLocator.isVisible()) { - console.log(' Already in library! Nothing to claim.'); - notify_game.status = 'existed'; - db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed - } else if (await getBtnLocator.isVisible()) { - console.log(' Not in library yet! Click GET.'); - await getBtnLocator.click(); - - // wait for popup - await page - .locator('iframe[name="purchase-sdk-hosted-iframe"]') - .waitFor(); - const popupLocator = page.frameLocator( - '[name=purchase-sdk-hosted-iframe]', - ); - - const finalGetBtnLocator = popupLocator.getByText('GET'); - await finalGetBtnLocator.waitFor(); - await finalGetBtnLocator.click(); - - await page.getByText('Thank you for your purchase.').waitFor(); - notify_game.status = 'claimed'; - db.data[user][game_id].status = 'claimed'; - db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time - console.log(' Claimed successfully!'); - } - - // notify_game.status = db.data[user][game_id].status; // claimed or failed - - // const p = path.resolve(cfg.dir.screenshots, playstation-plus', `${game_id}.png`); - // if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... - } -} From e154b74e4d3a927f230d39fd63b374f92f37f7ef Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Jun 2024 14:09:37 +0200 Subject: [PATCH 23/33] eg: TODO handle base game for add-ons via function --- epic-games.js | 1 + 1 file changed, 1 insertion(+) diff --git a/epic-games.js b/epic-games.js index 70807b8..9aeb6ab 100644 --- a/epic-games.js +++ b/epic-games.js @@ -193,6 +193,7 @@ try { 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 urls.push(baseUrl); // add base game to the list of games to claim urls.push(url); // add add-on itself again } else { // GET From 00275d825baa6f7ddc04f74b091b4639e7948212 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Jun 2024 15:40:45 +0200 Subject: [PATCH 24/33] eg: forgot to catch timeout for captcha detection --- epic-games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epic-games.js b/epic-games.js index 9aeb6ab..04a091e 100644 --- a/epic-games.js +++ b/epic-games.js @@ -263,7 +263,7 @@ try { 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 db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time From aee72327eaf06600ae43c80799188b58d9074ddf Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Jun 2024 15:41:58 +0200 Subject: [PATCH 25/33] eg: disable webgl since it leaks running virtualized, #183 --- epic-games.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/epic-games.js b/epic-games.js index 04a091e..d0b9a03 100644 --- a/epic-games.js +++ b/epic-games.js @@ -1,7 +1,7 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra import { authenticator } from 'otplib'; import path from 'path'; -import { existsSync, writeFileSync } from 'fs'; +import { existsSync, writeFileSync, appendFileSync } from 'fs'; import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js'; import { cfg } from './src/config.js'; @@ -16,6 +16,14 @@ const db = await jsonDb('epic-games.json', {}); if (cfg.time) console.time('startup'); +const browserPrefs = path.join(cfg.dir.browser, 'prefs.js'); +if (existsSync(browserPrefs)) { + console.log('Adding webgl.disabled to', browserPrefs); + appendFileSync(browserPrefs, 'user_pref("webgl.disabled", true);'); // apparently Firefox removes duplicates (and sorts), so no problem appending every time +} else { + console.log(browserPrefs, 'does not exist yet, will patch it on next run. Restart the script if you get a captcha.'); +} + // https://playwright.dev/docs/auth#multi-factor-authentication const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, From 17f1ee41c975e1dc9d8a7e5abef254a17bf07349 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 27 Jun 2024 23:32:15 +0200 Subject: [PATCH 26/33] eg: don't exit on login error since it may be 'Incorrect response' for captcha https://github.com/vogler/free-games-claimer/issues/183#issuecomment-2195691372 --- epic-games.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/epic-games.js b/epic-games.js index d0b9a03..9d492a0 100644 --- a/epic-games.js +++ b/epic-games.js @@ -103,7 +103,7 @@ try { await notify('epic-games: got captcha during login. Please check.'); }).catch(_ => { }); page.waitForSelector('p:has-text("Incorrect response.")').then(async () => { - console.error('Incorrect repsonse for captcha!'); + 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 @@ -116,8 +116,7 @@ try { const error = page.locator('#form-error-message'); error.waitFor().then(async () => { console.error('Login error:', await error.innerText()); - await context.close(); // finishes potential recording - process.exit(1); + console.log('Please login in the browser!'); }).catch(_ => { }); // handle MFA, but don't await it page.waitForURL('**/id/login/mfa**').then(async () => { From b8f7068a873d47ccc3c657c2c82aa1bc718cc455 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Wed, 10 Jul 2024 13:27:48 +0200 Subject: [PATCH 27/33] pg: scrollUntilStable to wait in loop to load all games since one scroll may not be enough --- prime-gaming.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/prime-gaming.js b/prime-gaming.js index 5e3e283..eeface0 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -97,12 +97,25 @@ try { process.exit(1); } + const waitUntilStable = async (f, act) => { + let v; + while (true) { + const v2 = await f(); + if (v == v2) break; + v = v2; + await act(); + } + }; + const scrollUntilStable = async f => waitUntilStable(f, async () => { + await page.keyboard.press('End'); // scroll to bottom to show all games + await page.waitForLoadState('networkidle'); // wait for all games to be loaded + await page.waitForTimeout(2000); // TODO networkidle wasn't enough to load all already collected games + }); + await page.click('button[data-type="Game"]'); - await page.keyboard.press('End'); // scroll to bottom to show all games - await page.waitForLoadState('networkidle'); // wait for all games to be loaded - await page.waitForTimeout(2000); // TODO networkidle wasn't enough to load all already collected games const games = page.locator('div[data-a-target="offer-list-FGWP_FULL"]'); await games.waitFor(); + await scrollUntilStable(() => games.locator('.item-card__action').count()); console.log('Number of already claimed games (total):', await games.locator('p:has-text("Collected")').count()); // 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(); @@ -324,8 +337,7 @@ try { if (notify_games.length) { // make screenshot of all games if something was claimed const p = screenshot(`${filenamify(datetime())}.png`); // await page.screenshot({ path: p, fullPage: true }); // fullPage does not make a difference since scroll not on body but on some element - await page.keyboard.press('End'); // scroll to bottom to show all games - await page.waitForTimeout(1000); // wait for fade in animation + await scrollUntilStable(() => games.locator('.item-card__action').count()); const viewportSize = page.viewportSize(); // current viewport size await page.setViewportSize({ ...viewportSize, height: 3000 }); // increase height, otherwise element screenshot is cut off at the top and bottom await games.screenshot({ path: p }); // screenshot of all claimed games @@ -339,17 +351,7 @@ try { await loot.waitFor(); process.stdout.write('Loading all DLCs on page...'); - let n1 = 0; - let n2 = 0; - do { - n1 = n2; - n2 = await loot.locator('[data-a-target="item-card"]').count(); - // console.log(n2); - process.stdout.write(` ${n2}`); - await page.keyboard.press('End'); // scroll to bottom to show all dlcs - await page.waitForLoadState('networkidle'); // did not wait for dlcs to be loaded - await page.waitForTimeout(1000); - } while (n2 > n1); + scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count()) console.log('\nNumber of already claimed DLC:', await loot.locator('p:has-text("Collected")').count()); From a39d737999c509b9065ee07dd7a806e6579ff5e6 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Fri, 19 Jul 2024 14:35:40 +0200 Subject: [PATCH 28/33] aliexpress: collect daily coins via desktop website All load in webview in android app. Other games have no desktop version and mobile version uses canvas and only refers to download app with both stealth and fingerprint-injector. Also stuck on loading screen when using firefox instead of chrome. --- aliexpress.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.js | 3 ++ 2 files changed, 108 insertions(+) create mode 100644 aliexpress.js diff --git a/aliexpress.js b/aliexpress.js new file mode 100644 index 0000000..c44c673 --- /dev/null +++ b/aliexpress.js @@ -0,0 +1,105 @@ +import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra +import { datetime, filenamify, prompt, handleSIGINT, stealth } from './src/util.js'; +import { cfg } from './src/config.js'; + +const context = await firefox.launchPersistentContext(cfg.dir.browser, { + headless: cfg.headless, + viewport: { width: cfg.width, height: cfg.height }, + locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL + recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 + recordHar: cfg.record ? { path: `data/record/gog-${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(context); +await stealth(context); + +context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout); + +const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist + +const auth = async (url) => { + console.log('auth', url); + await page.goto(url, { waitUntil: 'domcontentloaded' }); + // redirects to https://login.aliexpress.com/?return_url=https%3A%2F%2Fwww.aliexpress.com%2Fp%2Fcoin-pc-index%2Findex.html + await Promise.any([page.waitForURL(/.*login.aliexpress.com.*/).then(async () => { + // manual login + console.error('Not logged in! Will wait for 120s for you to login...'); + // await page.waitForTimeout(120*1000); + // or try automated + page.locator('span:has-text("Switch account")').click().catch(_ => {}); // sometimes no longer logged in, but previous user/email is pre-selected -> in this case we want to go back to the classic login + const login = page.locator('.login-container'); + const email = cfg.ae_email || await prompt({ message: 'Enter email' }); + const emailInput = login.locator('input[label="Email or phone number"]'); + await emailInput.fill(email); + await emailInput.blur(); // otherwise Continue button stays disabled + const continueButton = login.locator('button:has-text("Continue")'); + await continueButton.click({ force: true }); // normal click waits for button to no longer be covered by their suggestion menu, so we have to force click somewhere for the menu to close and then click + await continueButton.click(); + const password = email && (cfg.ae_password || await prompt({ type: 'password', message: 'Enter password' })); + await login.locator('input[label="Password"]').fill(password); + await login.locator('button:has-text("Sign in")').click(); + const error = login.locator('.error-text'); + error.waitFor().then(async _ => console.error('Login error:', await error.innerText())); + await page.waitForURL(url); + // await page.addLocatorHandler(page.getByRole('button', { name: 'Accept cookies' }), btn => btn.click()); + page.getByRole('button', { name: 'Accept cookies' }).click().then(_ => console.log('Accepted cookies')).catch(_ => { }); + }), page.locator('#nav-user-account').waitFor()]).catch(_ => {}); + + // await page.locator('#nav-user-account').hover(); + // console.log('Logged in as:', await page.locator('.welcome-name').innerText()); +}; + +// copied URLs from AliExpress app on tablet which has menu for the used webview +const urls = { + // works with desktop view, but stuck at 100% loading in mobile view: + coins: 'https://www.aliexpress.com/p/coin-pc-index/index.html', + // only work with mobile view: + grow: 'https://m.aliexpress.com/p/ae_fruit/index.html', // firefox: stuck at 60% loading, chrome: loads, but canvas + gogo: 'https://m.aliexpress.com/p/gogo-match-cc/index.html', // closes firefox?! + // only show notification to install the app + euro: 'https://m.aliexpress.com/p/european-cup/index.html', // doesn't load + merge: 'https://m.aliexpress.com/p/merge-market/index.html', +}; + +const coins = async () => { + // await auth(urls.coins); + await Promise.any([page.locator('.checkin-button').click(), page.locator('.addcoin').waitFor()]); + console.log('Coins:', await page.locator('.mycoin-content-right-money').innerText()); + console.log('Streak:', await page.locator('.title-box').innerText()); + console.log('Tomorrow:', await page.locator('.addcoin').innerText()); +}; + +const grow = async () => { + await page.pause(); +}; + +const gogo = async () => { + await page.pause(); +}; + +const euro = async () => { + await page.pause(); +}; + +const merge = async () => { + await page.pause(); +}; + +try { + // await coins(); + await [ + coins, + // grow, + // gogo, + // euro, + // merge, + ].reduce((a, f) => a.then(async _ => { await auth(urls[f.name]); await f(); console.log() }), Promise.resolve()); + + // await page.pause(); +} catch (error) { + process.exitCode ||= 1; + console.error('--- Exception:'); + console.error(error); // .toString()? +} +if (page.video()) console.log('Recorded video:', await page.video().path()); +await context.close(); diff --git a/src/config.js b/src/config.js index 03414a0..4a384b8 100644 --- a/src/config.js +++ b/src/config.js @@ -41,6 +41,9 @@ export const cfg = { gog_email: process.env.GOG_EMAIL || process.env.EMAIL, gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD, gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game + // auth AliExpress + ae_email: process.env.AE_EMAIL || process.env.EMAIL, + ae_password: process.env.AE_PASSWORD || process.env.PASSWORD, // OTP only via GOG_EMAIL, can't add app... // experimmental pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores From d9e91d22c9f01114f905b8edf6e7322bb56639be Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Thu, 1 Aug 2024 15:46:41 +0200 Subject: [PATCH 29/33] eg: fix changed button locators --- epic-games.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/epic-games.js b/epic-games.js index 9d492a0..ff8f3e0 100644 --- a/epic-games.js +++ b/epic-games.js @@ -156,7 +156,8 @@ try { for (const url of urls) { if (cfg.time) console.time('claim game'); await page.goto(url); // , { waitUntil: 'domcontentloaded' }); - const btnText = await page.locator('//button[@data-testid="purchase-cta-button"][not(contains(.,"Loading"))]').first().innerText(); // barrier to block until page is loaded + const purcahseBtn = page.locator('aside button').first(); + const btnText = (await purcahseBtn.innerText()).toLowerCase(); // barrier to block until page is loaded // click Continue if 'This game contains mature content recommended only for ages 18+' if (await page.locator('button:has-text("Continue")').count() > 0) { @@ -187,12 +188,12 @@ try { const notify_game = { title, url, status: 'failed' }; notify_games.push(notify_game); // status is updated below - if (btnText.toLowerCase() == 'in library') { + if (btnText == 'in library') { console.log(' Already in library! Nothing to claim.'); notify_game.status = 'existed'; db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed if (db.data[user][game_id].status.startsWith('failed')) db.data[user][game_id].status = 'manual'; // was failed but now it's claimed - } else if (btnText.toLowerCase() == 'requires base game') { + } else if (btnText == 'requires base game') { console.log(' Requires base game! Nothing to claim.'); notify_game.status = 'requires base game'; db.data[user][game_id].status ||= 'failed:requires-base-game'; @@ -205,7 +206,7 @@ try { urls.push(url); // add add-on itself again } else { // 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 purcahseBtn.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? @@ -252,7 +253,7 @@ try { await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 }); // 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 Accept")'); btnAgree.waitFor().then(() => 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? From 8605b7037707addbd98643bb3e454fedb1951917 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Fri, 2 Aug 2024 12:57:37 +0200 Subject: [PATCH 30/33] pg: fix internal/external locators, closes #355, waitUntilStable 2s -> 5s --- prime-gaming.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/prime-gaming.js b/prime-gaming.js index eeface0..b902712 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -101,6 +101,7 @@ try { let v; while (true) { const v2 = await f(); + console.log('waitUntilStable', v2); if (v == v2) break; v = v2; await act(); @@ -109,7 +110,7 @@ try { const scrollUntilStable = async f => waitUntilStable(f, async () => { await page.keyboard.press('End'); // scroll to bottom to show all games await page.waitForLoadState('networkidle'); // wait for all games to be loaded - await page.waitForTimeout(2000); // TODO networkidle wasn't enough to load all already collected games + await page.waitForTimeout(5000); // TODO networkidle wasn't enough to load all already collected games }); await page.click('button[data-type="Game"]'); @@ -118,8 +119,8 @@ try { await scrollUntilStable(() => games.locator('.item-card__action').count()); console.log('Number of already claimed games (total):', await games.locator('p:has-text("Collected")').count()); // 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 external = await games.locator('.item-card__action:has([data-a-target="ExternalOfferClaim"])').all(); + const internal = await games.locator('.item-card__action:has(button[data-a-target="FGWPOffer"])').elementHandles(); + const external = await games.locator('.item-card__action:has(a[data-a-target="FGWPOffer"])').all(); // bottom to top: oldest to newest games internal.reverse(); external.reverse(); From eac11e8949c398c870b63d0e3d0d56f6c74e0692 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Fri, 2 Aug 2024 13:06:28 +0200 Subject: [PATCH 31/33] ci/docker: force tag main as latest since many people pull it --- .github/workflows/docker.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a54117c..34c0285 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -34,6 +34,9 @@ jobs: else echo "IMAGE_TAG=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV fi + # TODO the above didn't tag main as latest... probably not available in env.BRANCH right away + echo "${{ env.BRANCH }}" + echo "IMAGE_TAG=latest" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 23a611a2f11569ed20c189d0d51d45ffa93de403 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Fri, 2 Aug 2024 13:16:54 +0200 Subject: [PATCH 32/33] ci/docker: fix: tag main as latest --- .github/workflows/docker.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 34c0285..5e5adba 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -27,16 +27,14 @@ jobs: - name: Set environment variables run: | - echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + BRANCH="${GITHUB_REF#refs/heads/}" + echo "BRANCH=$BRANCH" >> $GITHUB_ENV echo "NOW=$(date -R)" >> $GITHUB_ENV # date -Iseconds; date +'%Y-%m-%dT%H:%M:%S' - if [[ "${{ env.BRANCH }}" == "main" ]]; then + if [[ "$BRANCH" == "main" ]]; then echo "IMAGE_TAG=latest" >> $GITHUB_ENV else - echo "IMAGE_TAG=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + echo "IMAGE_TAG=$BRANCH" >> $GITHUB_ENV fi - # TODO the above didn't tag main as latest... probably not available in env.BRANCH right away - echo "${{ env.BRANCH }}" - echo "IMAGE_TAG=latest" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v3 From c87deb393eec26b78d7a30bded8859c183ebb278 Mon Sep 17 00:00:00 2001 From: Ralf Vogler Date: Fri, 2 Aug 2024 14:02:33 +0200 Subject: [PATCH 33/33] pg: DLC: forgot to await scrollUntilStable, closes #356 --- prime-gaming.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prime-gaming.js b/prime-gaming.js index b902712..88ae542 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -352,7 +352,7 @@ try { await loot.waitFor(); process.stdout.write('Loading all DLCs on page...'); - scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count()) + await scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count()) console.log('\nNumber of already claimed DLC:', await loot.locator('p:has-text("Collected")').count());