Merge pull request #479 from vogler/patchright
Fixes #183. See #470 for more details. Tested: - epic-games - works without captcha now - prime-gaming - seems to work, didn't test claiming on all the external stores, some patchright-related issues may come up - gog - works for me, but had the most issues with patchright, may fail on slow machines where I inserted 2s waitForTimeout as a quick fix - aliexpress - claiming coins should work, login is broken (unrelated) - unrealengine - login failed, didn't test rest, needs to be updated/merged anyway (unrelated) Only tested epic-games in docker, rest without, but should behave the same.
This commit is contained in:
commit
b959bf5330
11 changed files with 207 additions and 882 deletions
53
Dockerfile
53
Dockerfile
|
|
@ -1,5 +1,5 @@
|
||||||
# FROM mcr.microsoft.com/playwright:v1.20.0
|
# FROM mcr.microsoft.com/playwright:v1.20.0
|
||||||
# Partially from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal
|
# Partially from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.jammy
|
||||||
FROM ubuntu:jammy
|
FROM ubuntu:jammy
|
||||||
|
|
||||||
# Configuration variables are at the end!
|
# Configuration variables are at the end!
|
||||||
|
|
@ -8,9 +8,9 @@ FROM ubuntu:jammy
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install up-to-date node & npm, deps for virtual screen & noVNC, firefox, pip for apprise.
|
# Install nodejs and deps for virtual display, noVNC, chromium, and pip for installing apprise.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install --no-install-recommends -y curl ca-certificates gnupg \
|
&& apt-get install -y --no-install-recommends curl ca-certificates gnupg \
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
|
@ -23,43 +23,38 @@ RUN apt-get update \
|
||||||
novnc websockify \
|
novnc websockify \
|
||||||
dos2unix \
|
dos2unix \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
# && npx playwright install-deps firefox \
|
# RUN npx patchright install-deps chromium
|
||||||
&& apt-get install --no-install-recommends -y \
|
# ^ installing deps manually instead saved ~130MB:
|
||||||
libgtk-3-0 \
|
&& apt-get install -y --no-install-recommends \
|
||||||
libasound2 \
|
libnss3 \
|
||||||
libxcomposite1 \
|
libnspr4 \
|
||||||
libpangocairo-1.0-0 \
|
|
||||||
libpango-1.0-0 \
|
|
||||||
libatk1.0-0 \
|
libatk1.0-0 \
|
||||||
libcairo-gobject2 \
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libatspi2.0-0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libgbm1 \
|
||||||
|
libpango-1.0-0 \
|
||||||
libcairo2 \
|
libcairo2 \
|
||||||
libgdk-pixbuf-2.0-0 \
|
libasound2 \
|
||||||
libdbus-glib-1-2 \
|
|
||||||
libxcursor1 \
|
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
|
# https://www.perplexity.ai/search/what-files-do-i-need-to-remove-imjwdphNSUWK98WzsmQswA
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf \
|
&& rm -rf \
|
||||||
|
/var/lib/apt/lists/* \
|
||||||
|
/var/cache/* \
|
||||||
|
/var/tmp/* \
|
||||||
/tmp/* \
|
/tmp/* \
|
||||||
/usr/share/doc/* \
|
/usr/share/doc/* \
|
||||||
/var/cache/* \
|
&& ln -s /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \
|
||||||
/var/lib/apt/lists/* \
|
&& pip install --no-cache-dir apprise
|
||||||
/var/tmp/*
|
|
||||||
|
|
||||||
# RUN node --version
|
|
||||||
# RUN npm --version
|
|
||||||
|
|
||||||
RUN ln -s /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html
|
|
||||||
RUN pip install --no-cache-dir apprise
|
|
||||||
|
|
||||||
WORKDIR /fgc
|
WORKDIR /fgc
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Playwright installs patched firefox to ~/.cache/ms-playwright/firefox-*
|
# --no-shell to avoid installing chromium_headless_shell (307MB) since headless mode could be detected without patching the browser itself
|
||||||
# Requires some system deps to run (see inlined install-deps above).
|
RUN npm install && npx patchright install chromium --no-shell && du -h -d1 ~/.cache/ms-playwright
|
||||||
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 . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,13 @@ Data (including json files with claimed games, codes to redeem, screenshots) is
|
||||||
|
|
||||||
1. [Install Node.js](https://nodejs.org/en/download)
|
1. [Install Node.js](https://nodejs.org/en/download)
|
||||||
2. Clone/download this repository and `cd` into it in a terminal
|
2. Clone/download this repository and `cd` into it in a terminal
|
||||||
3. Run `npm install`
|
3. Run `npm install && npx patchright install chromium`
|
||||||
4. Run `pip install apprise` (or use [pipx](https://github.com/pypa/pipx) if you have [problems](https://stackoverflow.com/questions/75608323/how-do-i-solve-error-externally-managed-environment-every-time-i-use-pip-3)) to install [apprise](https://github.com/caronc/apprise) if you want notifications
|
4. Run `pip install apprise` (or use [pipx](https://github.com/pypa/pipx) if you have [problems](https://stackoverflow.com/questions/75608323/how-do-i-solve-error-externally-managed-environment-every-time-i-use-pip-3)) to install [apprise](https://github.com/caronc/apprise) if you want notifications
|
||||||
5. To get updates: `git pull; npm install`
|
5. To get updates: `git pull; npm install`
|
||||||
6. Run `node epic-games`, `node prime-gaming`, `node gog`...
|
6. Run `node epic-games`, `node prime-gaming`, `node gog`...
|
||||||
|
|
||||||
During `npm install` Playwright will download its Firefox to a cache in home ([doc](https://playwright.dev/docs/browsers#managing-browser-binaries)).
|
Patchright/Playwright will download its Chromium to a cache in home ([doc](https://playwright.dev/docs/browsers#managing-browser-binaries)).
|
||||||
If you are missing some dependencies for the browser on your system, you can use `sudo npx playwright install firefox --with-deps`.
|
If you are missing some dependencies for the browser on your system, you can use `sudo npx patchright install chromium --with-deps`.
|
||||||
|
|
||||||
If you don't want to use Docker for quasi-headless mode, you could run inside a virtual machine, on a server, or you wake your PC at night to avoid being interrupted.
|
If you don't want to use Docker for quasi-headless mode, you could run inside a virtual machine, on a server, or you wake your PC at night to avoid being interrupted.
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { firefox } from 'playwright-firefox';
|
||||||
|
import { chromium } from 'patchright';
|
||||||
import { datetime, filenamify, prompt, handleSIGINT } from './src/util.js';
|
import { datetime, filenamify, prompt, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
||||||
// using https://github.com/apify/fingerprint-suite worked, but has no launchPersistentContext...
|
// can probably be removed and hard-code headers for mobile view
|
||||||
// from https://github.com/apify/fingerprint-suite/issues/162
|
|
||||||
import { FingerprintInjector } from 'fingerprint-injector';
|
import { FingerprintInjector } from 'fingerprint-injector';
|
||||||
import { FingerprintGenerator } from 'fingerprint-generator';
|
import { FingerprintGenerator } from 'fingerprint-generator';
|
||||||
|
|
||||||
|
|
@ -12,13 +12,14 @@ const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
|
||||||
operatingSystems: ['android'],
|
operatingSystems: ['android'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
headless: cfg.headless,
|
headless: cfg.headless,
|
||||||
// viewport: { width: cfg.width, height: cfg.height },
|
// 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
|
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
|
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/aliexpress-${filenamify(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/aliexpress-${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: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
|
// e.g. for coins, mobile view is needed, otherwise it just says to install the app
|
||||||
userAgent: fingerprint.navigator.userAgent,
|
userAgent: fingerprint.navigator.userAgent,
|
||||||
viewport: {
|
viewport: {
|
||||||
width: fingerprint.screen.width,
|
width: fingerprint.screen.width,
|
||||||
|
|
@ -27,6 +28,10 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
||||||
extraHTTPHeaders: {
|
extraHTTPHeaders: {
|
||||||
'accept-language': headers['accept-language'],
|
'accept-language': headers['accept-language'],
|
||||||
},
|
},
|
||||||
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
|
args: [
|
||||||
|
'--hide-crash-restore-bubble',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
handleSIGINT(context);
|
handleSIGINT(context);
|
||||||
// await stealth(context);
|
// await stealth(context);
|
||||||
|
|
@ -81,11 +86,14 @@ const urls = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const coins = async () => {
|
const coins = async () => {
|
||||||
// await auth(urls.coins);
|
const collectBtn = page.locator('div:has-text("Collect")').first();
|
||||||
await Promise.any([page.locator('.checkin-button').click(), page.locator('.addcoin').waitFor()]);
|
const moreBtn = page.locator('div:has-text("Earn more coins")').first();
|
||||||
console.log('Coins:', await page.locator('.mycoin-content-right-money').innerText());
|
await Promise.any([collectBtn.click(), moreBtn.waitFor()]);
|
||||||
console.log('Streak:', await page.locator('.title-box').innerText());
|
console.log(await page.locator('.marquee-content:has-text(" coins")').first().innerText());
|
||||||
console.log('Tomorrow:', await page.locator('.addcoin').innerText());
|
const n = (await page.locator('.marquee-item:has-text(" coins")').first().innerText()).replace(' coins', '');
|
||||||
|
console.log('Coins:', n);
|
||||||
|
// console.log('Streak:', await page.locator('.title-box').innerText());
|
||||||
|
// console.log('Tomorrow:', await page.locator('.addcoin').innerText());
|
||||||
};
|
};
|
||||||
|
|
||||||
// const grow = async () => {
|
// const grow = async () => {
|
||||||
|
|
|
||||||
|
|
@ -6,28 +6,16 @@ echo "Version: https://github.com/vogler/free-games-claimer/tree/${COMMIT}"
|
||||||
[ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && echo "Branch: ${BRANCH}"
|
[ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && echo "Branch: ${BRANCH}"
|
||||||
echo "Build: $NOW"
|
echo "Build: $NOW"
|
||||||
|
|
||||||
|
BROWSER="${BROWSER_DIR:-data/browser}"
|
||||||
|
|
||||||
# Remove chromium profile lock.
|
# Remove chromium profile lock.
|
||||||
# When running in docker and then killing it, on the next run chromium displayed a dialog to unlock the profile which made the script time out.
|
# When running in docker and then killing it, on the next run chromium displayed a dialog to unlock the profile which made the script time out.
|
||||||
# Maybe due to changed hostname of container or due to how the docker container kills playwright - didn't check.
|
# Maybe due to changed hostname of container or due to how the docker container kills playwright - didn't check.
|
||||||
# https://bugs.chromium.org/p/chromium/issues/detail?id=367048
|
# https://bugs.chromium.org/p/chromium/issues/detail?id=367048
|
||||||
rm -f /fgc/data/browser/SingletonLock
|
rm -f "/fgc/$BROWSER/SingletonLock"
|
||||||
|
|
||||||
# Firefox preferences are stored in $BROWSER_DIR/pref.js and can be overridden by a file user.js
|
|
||||||
# Since this file has to be in the volume (data/browser), we can't do this in Dockerfile.
|
|
||||||
mkdir -p /fgc/data/browser
|
|
||||||
# fix for 'Incorrect response' after solving a captcha correctly - https://github.com/vogler/free-games-claimer/issues/261#issuecomment-1868385830
|
|
||||||
# echo 'user_pref("privacy.resistFingerprinting", true);' > /fgc/data/browser/user.js
|
|
||||||
cat <<EOT >/fgc/data/browser/user.js
|
|
||||||
user_pref("privacy.resistFingerprinting", true);
|
|
||||||
// user_pref("privacy.resistFingerprinting.letterboxing", true);
|
|
||||||
// user_pref("browser.contentblocking.category", "strict");
|
|
||||||
// user_pref("webgl.disabled", true);
|
|
||||||
EOT
|
|
||||||
# TODO disable session restore message?
|
|
||||||
|
|
||||||
# Remove X server display lock, fix for `docker compose up` which reuses container which made it fail after initial run, https://github.com/vogler/free-games-claimer/issues/31
|
# Remove X server display lock, fix for `docker compose up` which reuses container which made it fail after initial run, https://github.com/vogler/free-games-claimer/issues/31
|
||||||
# echo $DISPLAY
|
# Maybe no longer needed after adding #478's -nolisten unix below
|
||||||
# ls -l /tmp/.X11-unix/
|
|
||||||
rm -f /tmp/.X1-lock
|
rm -f /tmp/.X1-lock
|
||||||
|
|
||||||
# 6000+SERVERNUM is the TCP port Xvfb is listening on:
|
# 6000+SERVERNUM is the TCP port Xvfb is listening on:
|
||||||
|
|
@ -36,10 +24,11 @@ rm -f /tmp/.X1-lock
|
||||||
# Options passed directly to the Xvfb server:
|
# Options passed directly to the Xvfb server:
|
||||||
# -ac disables host-based access control mechanisms
|
# -ac disables host-based access control mechanisms
|
||||||
# −screen NUM WxHxD creates the screen and sets its width, height, and depth
|
# −screen NUM WxHxD creates the screen and sets its width, height, and depth
|
||||||
|
# -nolisten unix tells the server not to use Unix domain sockets, thus avoiding the need to create /tmp/.X11-unix
|
||||||
|
|
||||||
export DISPLAY=:1 # need to export this, otherwise playwright complains with 'Looks like you launched a headed browser without having a XServer running.'
|
export DISPLAY=:1 # need to export this, otherwise playwright complains with 'Looks like you launched a headed browser without having a XServer running.'
|
||||||
Xvfb $DISPLAY -ac -screen 0 "${WIDTH}x${HEIGHT}x${DEPTH}" &
|
Xvfb $DISPLAY -ac -screen 0 "${WIDTH}x${HEIGHT}x${DEPTH}" &
|
||||||
echo "Xvfb display server created screen with resolution ${WIDTH}x${HEIGHT}"
|
echo "Xvfb display server created screen with resolution ${WIDTH}x${HEIGHT} -nolisten unix"
|
||||||
if [ -z "$VNC_PASSWORD" ]; then
|
if [ -z "$VNC_PASSWORD" ]; then
|
||||||
pw="-nopw"
|
pw="-nopw"
|
||||||
pwt="no password!"
|
pwt="no password!"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { chromium } from 'playwright-chromium';
|
||||||
|
import { chromium } from 'patchright';
|
||||||
import { authenticator } from 'otplib';
|
import { authenticator } from 'otplib';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { existsSync, writeFileSync, appendFileSync } from 'fs';
|
import { existsSync, writeFileSync } from 'fs';
|
||||||
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
import { resolve, jsonDb, datetime, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
||||||
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
||||||
|
|
@ -17,35 +18,27 @@ const db = await jsonDb('epic-games.json', {});
|
||||||
|
|
||||||
if (cfg.time) console.time('startup');
|
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
|
// https://playwright.dev/docs/auth#multi-factor-authentication
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
headless: cfg.headless,
|
// channel: 'chrome', // recommended, but `npx patchright install chrome` clashes with system Chrome - https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-nodejs#best-practice----use-chrome-without-fingerprint-injection
|
||||||
|
headless: false, // don't use cfg.headless headless here since SHOW=0 will lead to captcha
|
||||||
viewport: { width: cfg.width, height: cfg.height },
|
viewport: { width: cfg.width, height: cfg.height },
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated?
|
// locale: 'en-US', // ignore OS locale to be sure to have english text for locators
|
||||||
// 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: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800
|
||||||
recordHar: cfg.record ? { path: `data/record/eg-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
|
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
|
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
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
args: [ // https://wiki.mozilla.org/Firefox/CommandLineOptions
|
args: [
|
||||||
// '-kiosk',
|
'--hide-crash-restore-bubble',
|
||||||
],
|
],
|
||||||
|
// The following makes the browser crash in docker with 'Chromium sandboxing failed!':
|
||||||
|
// chromiumSandbox: true, // https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/issues/52
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSIGINT(context);
|
// console.log(context.browser().browserType()); // browser is null...
|
||||||
|
if (cfg.debug) console.log(chromium.executablePath());
|
||||||
|
|
||||||
// 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.
|
handleSIGINT(context);
|
||||||
await stealth(context);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
|
|
||||||
|
|
@ -150,7 +143,7 @@ try {
|
||||||
// debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking.
|
// debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking.
|
||||||
// Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions
|
// Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions
|
||||||
// i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
|
// i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
|
||||||
const urlSlugs = await Promise.all((await game_loc.elementHandles()).map(a => a.getAttribute('href')));
|
const urlSlugs = await Promise.all((await game_loc.all()).map(a => a.getAttribute('href')));
|
||||||
const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
|
const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
|
||||||
console.log('Free games:', urls);
|
console.log('Free games:', urls);
|
||||||
|
|
||||||
|
|
|
||||||
26
gog.js
26
gog.js
|
|
@ -1,4 +1,5 @@
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { firefox } from 'playwright-firefox';
|
||||||
|
import { chromium } from 'patchright';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { resolve, jsonDb, datetime, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
import { resolve, jsonDb, datetime, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
@ -17,13 +18,17 @@ if (cfg.width < 1280) { // otherwise 'Sign in' and #menuUsername are hidden (but
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://playwright.dev/docs/auth#multi-factor-authentication
|
// https://playwright.dev/docs/auth#multi-factor-authentication
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
headless: cfg.headless,
|
headless: cfg.headless,
|
||||||
viewport: { width: cfg.width, height: cfg.height },
|
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
|
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
|
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
|
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: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
|
args: [
|
||||||
|
'--hide-crash-restore-bubble',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSIGINT(context);
|
handleSIGINT(context);
|
||||||
|
|
@ -44,9 +49,11 @@ try {
|
||||||
|
|
||||||
// page.click('#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll').catch(_ => { }); // does not work reliably, solved by setting CookieConsent above
|
// page.click('#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll').catch(_ => { }); // does not work reliably, solved by setting CookieConsent above
|
||||||
const signIn = page.locator('a:has-text("Sign in")').first();
|
const signIn = page.locator('a:has-text("Sign in")').first();
|
||||||
await Promise.any([signIn.waitFor(), page.waitForSelector('#menuUsername')]);
|
// TODO for the below signIn.waitFor(), patchright failed most of the time with: locator.waitFor: JSHandles can be evaluated only in the context they were created!
|
||||||
while (await signIn.isVisible()) {
|
// await Promise.any([signIn.waitFor(), page.waitForSelector('#menuUsername')]);
|
||||||
console.error('Not signed in anymore.');
|
const username = page.locator('#menuUsername').first();
|
||||||
|
while (await signIn.isVisible() && !await username.isVisible()) {
|
||||||
|
console.error('Not signed!');
|
||||||
if (cfg.nowait) process.exit(1);
|
if (cfg.nowait) process.exit(1);
|
||||||
await signIn.click();
|
await signIn.click();
|
||||||
// it then creates an iframe for the login
|
// it then creates an iframe for the login
|
||||||
|
|
@ -59,10 +66,14 @@ try {
|
||||||
const email = cfg.gog_email || await prompt({ message: 'Enter email' });
|
const email = cfg.gog_email || await prompt({ message: 'Enter email' });
|
||||||
const password = email && (cfg.gog_password || await prompt({ type: 'password', message: 'Enter password' }));
|
const password = email && (cfg.gog_password || await prompt({ type: 'password', message: 'Enter password' }));
|
||||||
if (email && password) {
|
if (email && password) {
|
||||||
iframe.locator('a[href="/logout"]').click().catch(_ => { }); // Click 'Change account' (email from previous login is set in some cookie)
|
// iframe.locator('a[href="/logout"]').click().catch(_ => { }); // Click 'Change account' (email from previous login is set in some cookie)
|
||||||
await iframe.locator('#login_username').fill(email);
|
// TODO above didn't work with patchright
|
||||||
|
if (!await iframe.locator('#login_username').isDisabled()) {
|
||||||
|
await iframe.locator('#login_username').fill(email);
|
||||||
|
}
|
||||||
await iframe.locator('#login_password').fill(password);
|
await iframe.locator('#login_password').fill(password);
|
||||||
await iframe.locator('#login_login').click();
|
await iframe.locator('#login_login').click();
|
||||||
|
await page.waitForTimeout(2000); // TODO patchright waits forever for MFA locator otherwise
|
||||||
// handle MFA, but don't await it
|
// handle MFA, but don't await it
|
||||||
iframe.locator('form[name=second_step_authentication]').waitFor().then(async () => {
|
iframe.locator('form[name=second_step_authentication]').waitFor().then(async () => {
|
||||||
console.log('Two-Step Verification - Enter security code');
|
console.log('Two-Step Verification - Enter security code');
|
||||||
|
|
@ -97,6 +108,7 @@ try {
|
||||||
db.data[user] ||= {};
|
db.data[user] ||= {};
|
||||||
|
|
||||||
const banner = page.locator('#giveaway');
|
const banner = page.locator('#giveaway');
|
||||||
|
await page.waitForTimeout(2000); // TODO patchright sometimes missed banner otherwise
|
||||||
if (!await banner.count()) {
|
if (!await banner.count()) {
|
||||||
console.log('Currently no free giveaway!');
|
console.log('Currently no free giveaway!');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
839
package-lock.json
generated
839
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -26,8 +26,7 @@
|
||||||
"fingerprint-injector": "^2.1.66",
|
"fingerprint-injector": "^2.1.66",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"playwright-firefox": "^1.52.0",
|
"patchright": "^1.52.4"
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^4.4.0",
|
"@stylistic/eslint-plugin": "^4.4.0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { chromium } from 'playwright-chromium';
|
||||||
|
import { chromium } from 'patchright';
|
||||||
import { authenticator } from 'otplib';
|
import { authenticator } from 'otplib';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
import { resolve, jsonDb, datetime, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
||||||
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a);
|
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a);
|
||||||
|
|
@ -14,20 +15,20 @@ console.log(datetime(), 'started checking prime-gaming');
|
||||||
const db = await jsonDb('prime-gaming.json', {});
|
const db = await jsonDb('prime-gaming.json', {});
|
||||||
|
|
||||||
// https://playwright.dev/docs/auth#multi-factor-authentication
|
// https://playwright.dev/docs/auth#multi-factor-authentication
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
headless: cfg.headless,
|
headless: cfg.headless,
|
||||||
viewport: { width: cfg.width, height: cfg.height },
|
viewport: { width: cfg.width, height: cfg.height },
|
||||||
locale: 'en-US', // ignore OS locale to be sure to have english text for locators
|
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: '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-${filenamify(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
|
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
|
args: [
|
||||||
|
'--hide-crash-restore-bubble',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSIGINT(context);
|
handleSIGINT(context);
|
||||||
|
|
||||||
// TODO test if needed
|
|
||||||
await stealth(context);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
|
|
||||||
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
|
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
|
||||||
|
|
@ -127,7 +128,7 @@ try {
|
||||||
await scrollUntilStable(() => page.evaluate(() => document.querySelector('.tw-full-width').scrollHeight)); // height may change during loading while number of games is still the same?
|
await scrollUntilStable(() => page.evaluate(() => document.querySelector('.tw-full-width').scrollHeight)); // height may change during loading while number of games is still the same?
|
||||||
console.log('Number of already claimed games (total):', await games.locator('p:has-text("Collected")').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
|
// 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(button[data-a-target="FGWPOffer"])').elementHandles();
|
const internal = await games.locator('.item-card__action:has(button[data-a-target="FGWPOffer"])').all();
|
||||||
const external = await games.locator('.item-card__action:has(a[data-a-target="FGWPOffer"])').all();
|
const external = await games.locator('.item-card__action:has(a[data-a-target="FGWPOffer"])').all();
|
||||||
// bottom to top: oldest to newest games
|
// bottom to top: oldest to newest games
|
||||||
internal.reverse();
|
internal.reverse();
|
||||||
|
|
@ -155,8 +156,8 @@ try {
|
||||||
// claim games in internal store
|
// claim games in internal store
|
||||||
for (const card of internal) {
|
for (const card of internal) {
|
||||||
await card.scrollIntoViewIfNeeded();
|
await card.scrollIntoViewIfNeeded();
|
||||||
const title = await (await card.$('.item-card-details__body__primary')).innerText();
|
const title = await (await card.locator('.item-card-details__body__primary')).innerText();
|
||||||
const slug = await (await card.$('a')).getAttribute('href');
|
const slug = await (await card.locator('a')).getAttribute('href');
|
||||||
const url = 'https://gaming.amazon.com' + slug.split('?')[0];
|
const url = 'https://gaming.amazon.com' + slug.split('?')[0];
|
||||||
console.log('Current free game:', chalk.blue(title));
|
console.log('Current free game:', chalk.blue(title));
|
||||||
if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue;
|
if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue;
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,24 @@
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { firefox } from 'playwright-firefox';
|
||||||
import { jsonDb, prompt } from './src/util.js';
|
import { chromium } from 'patchright';
|
||||||
|
import { datetime, filenamify, jsonDb, prompt } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
||||||
const db = await jsonDb('steam-games.json', {});
|
const db = await jsonDb('steam-games.json', {});
|
||||||
|
|
||||||
const user = cfg.steam_id || await prompt({ message: 'Enter Steam community id ("View my profile", then copy from URL)' });
|
const user = cfg.steam_id || await prompt({ message: 'Enter Steam community id ("View my profile", then copy from URL)' });
|
||||||
|
|
||||||
// using https://github.com/apify/fingerprint-suite worked, but has no launchPersistentContext...
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
// from https://github.com/apify/fingerprint-suite/issues/162
|
|
||||||
import { FingerprintInjector } from 'fingerprint-injector';
|
|
||||||
import { FingerprintGenerator } from 'fingerprint-generator';
|
|
||||||
|
|
||||||
const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
|
|
||||||
devices: ['desktop'],
|
|
||||||
operatingSystems: ['windows'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
|
||||||
headless: cfg.headless,
|
headless: cfg.headless,
|
||||||
// viewport: { width: cfg.width, height: cfg.height },
|
// 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
|
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
|
||||||
userAgent: fingerprint.navigator.userAgent,
|
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
|
||||||
viewport: {
|
recordHar: cfg.record ? { path: `data/record/steam-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
|
||||||
width: fingerprint.screen.width,
|
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
height: fingerprint.screen.height,
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
},
|
args: [
|
||||||
extraHTTPHeaders: {
|
'--hide-crash-restore-bubble',
|
||||||
'accept-language': headers['accept-language'],
|
],
|
||||||
},
|
|
||||||
});
|
});
|
||||||
// await stealth(context);
|
|
||||||
await new FingerprintInjector().attachFingerprintToPlaywright(context, { fingerprint, headers });
|
|
||||||
|
|
||||||
context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout);
|
context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
// TODO This is mostly a copy of epic-games.js
|
// TODO This is mostly a copy of epic-games.js
|
||||||
// New assets to claim every first Tuesday of a month.
|
// New assets to claim every first Tuesday of a month.
|
||||||
|
|
||||||
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
// import { firefox } from 'playwright-firefox';
|
||||||
|
import { chromium } from 'patchright';
|
||||||
import { authenticator } from 'otplib';
|
import { authenticator } from 'otplib';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
|
import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
|
|
||||||
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'unrealengine', ...a);
|
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'unrealengine', ...a);
|
||||||
|
|
@ -18,21 +19,21 @@ console.log(datetime(), 'started checking unrealengine');
|
||||||
const db = await jsonDb('unrealengine.json', {});
|
const db = await jsonDb('unrealengine.json', {});
|
||||||
|
|
||||||
// https://playwright.dev/docs/auth#multi-factor-authentication
|
// https://playwright.dev/docs/auth#multi-factor-authentication
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
const context = await chromium.launchPersistentContext(cfg.dir.browser, {
|
||||||
headless: cfg.headless,
|
headless: cfg.headless,
|
||||||
viewport: { width: cfg.width, height: cfg.height },
|
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?
|
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
|
||||||
// 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: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800
|
||||||
recordHar: cfg.record ? { path: `data/record/ue-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
|
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: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
|
// https://peter.sh/experiments/chromium-command-line-switches/
|
||||||
|
args: [
|
||||||
|
'--hide-crash-restore-bubble',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSIGINT(context);
|
handleSIGINT(context);
|
||||||
|
|
||||||
await stealth(context);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
|
|
||||||
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
|
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue