diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf7fc06..25470d5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set environment variables run: | @@ -27,27 +27,27 @@ jobs: echo "NOW=$(date -R)" >> $GITHUB_ENV # date -Iseconds; date +'%Y-%m-%dT%H:%M:%S' - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 # if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/Dockerfile b/Dockerfile index f686c55..7a291b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,9 +55,10 @@ WORKDIR /fgc COPY package*.json ./ # Playwright installs patched firefox to ~/.cache/ms-playwright/firefox-* -# Requires some system deps to run (see install-deps above). +# Requires some system deps to run (see inlined install-deps above). RUN npm install # Old: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD + install firefox (had to be done after `npm install` to get the correct version). Now: playwright-firefox as npm dep and `npm install` will only install that. +# From 1.38 Playwright will no longer install browser automatically for playwright, but apparently still for playwright-firefox: https://github.com/microsoft/playwright/releases/tag/v1.38.0 # RUN npx playwright install firefox COPY . . diff --git a/epic-games.js b/epic-games.js index 1ab81c6..16984a3 100644 --- a/epic-games.js +++ b/epic-games.js @@ -14,8 +14,6 @@ console.log(datetime(), 'started checking epic-games'); const db = await jsonDb('epic-games.json', {}); -handleSIGINT(); - if (cfg.time) console.time('startup'); // https://www.nopecha.com extension source from https://github.com/NopeCHA/NopeCHA/releases/tag/0.1.16 @@ -31,8 +29,9 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { // 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 - recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 + recordVideo: cfg.record ? { dir: path.resolve('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 + handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved args: [ // https://peter.sh/experiments/chromium-command-line-switches // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.' // '--restore-last-session', // does not apply for crash/killed @@ -43,6 +42,8 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { // ignoreDefaultArgs: ['--enable-automation'], // remove default arg that shows the info bar with 'Chrome is being controlled by automated test software.'. Since Chromeium 106 this leads to show another info bar with 'You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer.'. }); +handleSIGINT(context); + // Without stealth plugin, the website shows an hcaptcha on login with username/password and in the last step of claiming a game. It may have other heuristics like unsuccessful logins as well. After <6h (TBD) it resets to no captcha again. Getting a new IP also resets. await stealth(context); @@ -98,7 +99,7 @@ try { console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...'); // TODO locator for text (email or app?) const otp = cfg.eg_otpkey && authenticator.generate(cfg.eg_otpkey) || await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await page.type('input[name="code-input-0"]', otp.toString()); + await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); await page.click('button[type="submit"]'); }).catch(_ => { }); } else { @@ -195,7 +196,7 @@ try { console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.'); } - await iframe.locator('input.payment-pin-code__input').first().type(cfg.eg_parentalpin); + await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin); await iframe.locator('button:has-text("Continue")').click({ delay: 11 }); }).catch(_ => { }); @@ -248,8 +249,9 @@ try { } if (cfg.time) console.timeEnd('claim all games'); } catch (error) { - console.error(error); // .toString()? process.exitCode ||= 1; + console.error('--- Exception:'); + console.error(error); // .toString()? if (error.message && process.exitCode != 130) notify(`epic-games failed: ${error.message.split('\n')[0]}`); } finally { @@ -259,4 +261,5 @@ try { } } if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); +if (page.video()) console.log('Recorded video:', await page.video().path()) await context.close(); diff --git a/gog.js b/gog.js index 9fc8a39..e8992ba 100644 --- a/gog.js +++ b/gog.js @@ -1,4 +1,5 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra +import path from 'path'; import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './util.js'; import { cfg } from './config.js'; import path from "path"; @@ -12,17 +13,18 @@ console.log(datetime(), 'started checking gog'); const db = await jsonDb('gog.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 - // recordHar: { path: './data/gog.har' }, // https://toolbox.googleapps.com/apps/har_analyzer/ - // recordVideo: { dir: './data/videos' }, // console.log(await page.video().path()); + recordVideo: cfg.record ? { dir: path.resolve('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 + handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved }); +handleSIGINT(context); + if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist @@ -61,7 +63,7 @@ try { console.log('Two-Step Verification - Enter security code'); console.log(await iframe.locator('.form__description').innerText()) const otp = await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 4 || 'The code must be 4 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await iframe.locator('#second_step_authentication_token_letter_1').type(otp.toString(), {delay: 10}); + await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), {delay: 10}); await iframe.locator('#second_step_authentication_send').click(); await page.waitForTimeout(1000); // TODO still needed with wait for username below? }).catch(_ => { }); @@ -96,8 +98,9 @@ try { await claimFreegames(); } } catch (error) { - console.error(error); // .toString()? process.exitCode ||= 1; + console.error('--- Exception:'); + console.error(error); // .toString()? if (error.message && process.exitCode != 130) notify(`gog failed: ${error.message.split('\n')[0]}`); } finally { @@ -152,6 +155,7 @@ async function claimGiveaway() { if (status == 'claimed' && !cfg.gog_newsletter) { console.log("Unsubscribe from 'Promotions and hot deals' newsletter"); await page.goto('https://www.gog.com/en/account/settings/subscriptions'); + await page.locator('li:has-text("Marketing communications through Trusted Partners") label').uncheck(); await page.locator('li:has-text("Promotions and hot deals") label').uncheck(); } } @@ -291,4 +295,5 @@ function isNotClaimedUrl(url) { } } +if (page.video()) console.log('Recorded video:', await page.video().path()) await context.close(); diff --git a/package-lock.json b/package-lock.json index b40dc0e..8078e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "enquirer": "^2.4.1", "lowdb": "^6.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.37.1", + "playwright-firefox": "^1.38.0", "puppeteer-extra-plugin-stealth": "^2.11.2" } }, @@ -448,9 +448,9 @@ } }, "node_modules/playwright-core": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", - "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", "bin": { "playwright-core": "cli.js" }, @@ -459,12 +459,12 @@ } }, "node_modules/playwright-firefox": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.37.1.tgz", - "integrity": "sha512-I8QScyW+hjGltywqLNh3Y1W96/3x70el9wNneuI34l3uVhiCRt9Co27+kiL+UlA1V8MTzaMere3ONQ8lGeut5w==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.38.0.tgz", + "integrity": "sha512-uNXdvj17JHbKir/EmdLtYEHkzI0ttFMX/3+HO/TW5z1hRmN5CydDNINm9xL/0AwvFSa5unZPM7S7+mUa3EiniA==", "hasInstallScript": true, "dependencies": { - "playwright-core": "1.37.1" + "playwright-core": "1.38.0" }, "bin": { "playwright": "cli.js" @@ -1032,16 +1032,16 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "playwright-core": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.37.1.tgz", - "integrity": "sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==" + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==" }, "playwright-firefox": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.37.1.tgz", - "integrity": "sha512-I8QScyW+hjGltywqLNh3Y1W96/3x70el9wNneuI34l3uVhiCRt9Co27+kiL+UlA1V8MTzaMere3ONQ8lGeut5w==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-firefox/-/playwright-firefox-1.38.0.tgz", + "integrity": "sha512-uNXdvj17JHbKir/EmdLtYEHkzI0ttFMX/3+HO/TW5z1hRmN5CydDNINm9xL/0AwvFSa5unZPM7S7+mUa3EiniA==", "requires": { - "playwright-core": "1.37.1" + "playwright-core": "1.38.0" } }, "puppeteer-extra-plugin": { diff --git a/package.json b/package.json index 6179433..9a91f7f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "enquirer": "^2.4.1", "lowdb": "^6.0.1", "otplib": "^12.0.1", - "playwright-firefox": "^1.37.1", + "playwright-firefox": "^1.38.0", "puppeteer-extra-plugin-stealth": "^2.11.2" }, "repository": { diff --git a/prime-gaming.js b/prime-gaming.js index 1162496..8fcfa2b 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -1,5 +1,6 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra import { authenticator } from 'otplib'; +import path from 'path'; import { resolve, jsonDb, datetime, stealth, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './util.js'; import { cfg } from './config.js'; @@ -12,17 +13,18 @@ console.log(datetime(), 'started checking prime-gaming'); const db = await jsonDb('prime-gaming.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 - recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 + recordVideo: cfg.record ? { dir: path.resolve('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 + handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved }); +handleSIGINT(context); + // TODO test if needed await stealth(context); @@ -66,7 +68,7 @@ try { console.log('Two-Step Verification - enter the One Time Password (OTP), e.g. generated by your Authenticator App'); await page.check('[name=rememberDevice]'); const otp = cfg.pg_otpkey && authenticator.generate(cfg.pg_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=otpCode]', otp.toString()); + await page.locator('input[name=otpCode]').pressSequentially(otp.toString()); await page.click('input[type="submit"]'); }).catch(_ => { }); } else { @@ -377,8 +379,9 @@ try { console.log('DLC: Unlinked accounts:', dlc_unlinked); } } catch (error) { - console.error(error); // .toString()? process.exitCode ||= 1; + console.error('--- Exception:'); + console.error(error); // .toString()? if (error.message && process.exitCode != 130) notify(`prime-gaming failed: ${error.message.split('\n')[0]}`); } finally { @@ -387,4 +390,5 @@ try { notify(`prime-gaming (${user}):
${html_game_list(notify_games)}`); } } +if (page.video()) console.log('Recorded video:', await page.video().path()) await context.close(); diff --git a/unrealengine.js b/unrealengine.js index 4370a85..f7ee6fa 100644 --- a/unrealengine.js +++ b/unrealengine.js @@ -17,8 +17,6 @@ console.log(datetime(), 'started checking unrealengine'); const db = await jsonDb('unrealengine.json', {}); -handleSIGINT(); - // https://playwright.dev/docs/auth#multi-factor-authentication const context = await firefox.launchPersistentContext(cfg.dir.browser, { headless: cfg.headless, @@ -26,10 +24,13 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? // userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0 locale: "en-US", // ignore OS locale to be sure to have english text for locators - recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 + recordVideo: cfg.record ? { dir: path.resolve('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 + handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved }); +handleSIGINT(context); + await stealth(context); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); @@ -71,7 +72,7 @@ try { console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...'); // TODO locator for text (email or app?) const otp = cfg.eg_otpkey && authenticator.generate(cfg.eg_otpkey) || await prompt({type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!'}); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them - await page.type('input[name="code-input-0"]', otp.toString()); + await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); await page.click('button[type="submit"]'); }).catch(_ => { }); } else { @@ -188,8 +189,9 @@ try { console.log('Done'); } } catch (error) { - console.error(error); // .toString()? process.exitCode ||= 1; + console.error('--- Exception:'); + console.error(error); // .toString()? if (error.message && process.exitCode != 130) notify(`unrealengine failed: ${error.message.split('\n')[0]}`); } finally { @@ -199,4 +201,5 @@ try { } } if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); +if (page.video()) console.log('Recorded video:', await page.video().path()) await context.close(); diff --git a/util.js b/util.js index 60ff4a5..94cccfd 100644 --- a/util.js +++ b/util.js @@ -27,9 +27,10 @@ export const datetimeUTC = (d = new Date()) => d.toISOString().replace('T', ' ') export const datetime = (d = new Date()) => datetimeUTC(new Date(d.getTime() - d.getTimezoneOffset() * 60000)); export const filenamify = s => s.replaceAll(':', '.').replace(/[^a-z0-9 _\-.]/gi, '_'); // alternative: https://www.npmjs.com/package/filenamify - On Unix-like systems, / is reserved. On Windows, <>:"/\|?* along with trailing periods are reserved. -export const handleSIGINT = () => process.on('SIGINT', () => { // e.g. when killed by Ctrl-C +export const handleSIGINT = (context = null) => process.on('SIGINT', async () => { // e.g. when killed by Ctrl-C console.error('\nInterrupted by SIGINT. Exit!'); // Exception shows where the script was:\n'); // killed before catch in docker... process.exitCode = 130; // 128+SIGINT to indicate to parent that process was killed + if (context) await context.close(); // in order to save recordings also on SIGINT, we need to disable Playwright's handleSIGINT and close the context ourselves }); // stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212