Merge pull request #11 from jackblk/main
feat: dockerize, fix sign in loop
This commit is contained in:
commit
212930639f
7 changed files with 943 additions and 1309 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
userDataDir**
|
||||||
|
node_modules
|
||||||
|
screenshots
|
||||||
|
|
||||||
|
.gitignore
|
||||||
|
**Dockerfile**
|
||||||
|
.dockerignore
|
||||||
|
.env
|
||||||
|
auth.json
|
||||||
77
Dockerfile
Normal file
77
Dockerfile
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
# FROM mcr.microsoft.com/playwright:v1.20.0
|
||||||
|
FROM ubuntu:focal
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Configure Xvfb via environment variables:
|
||||||
|
ENV SCREEN_WIDTH 1440
|
||||||
|
ENV SCREEN_HEIGHT 900
|
||||||
|
ENV SCREEN_DEPTH 24
|
||||||
|
ENV DISPLAY :60
|
||||||
|
|
||||||
|
# Configure VNC via environment variables:
|
||||||
|
ENV VNC_ENABLED true
|
||||||
|
ENV VNC_PASSWORD secret
|
||||||
|
ENV VNC_PORT 5900
|
||||||
|
ENV NOVNC_PORT 6080
|
||||||
|
EXPOSE 5900
|
||||||
|
EXPOSE 6080
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||||
|
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD true
|
||||||
|
|
||||||
|
# === INSTALL Node.js ===
|
||||||
|
|
||||||
|
# Taken from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal
|
||||||
|
RUN apt-get update && \
|
||||||
|
# Install node16
|
||||||
|
apt-get install -y curl wget && \
|
||||||
|
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||||
|
apt-get install -y nodejs && \
|
||||||
|
# Feature-parity with node.js base images.
|
||||||
|
apt-get install -y --no-install-recommends git openssh-client && \
|
||||||
|
npm install -g yarn && \
|
||||||
|
# clean apt cache
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
# Create the pwuser
|
||||||
|
adduser pwuser
|
||||||
|
|
||||||
|
|
||||||
|
# === Install the base requirements to run and debug webdriver implementations ===
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||||
|
xvfb \
|
||||||
|
ca-certificates \
|
||||||
|
x11vnc \
|
||||||
|
curl \
|
||||||
|
tini \
|
||||||
|
novnc websockify \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf \
|
||||||
|
/tmp/* \
|
||||||
|
/usr/share/doc/* \
|
||||||
|
/var/cache/* \
|
||||||
|
/var/lib/apt/lists/* \
|
||||||
|
/var/tmp/*
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /fgc
|
||||||
|
COPY package.json .
|
||||||
|
# Install chromium & dependencies only
|
||||||
|
RUN npm install \
|
||||||
|
&& npx playwright install --with-deps chromium \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
RUN mv ./docker/entrypoint.sh /usr/local/bin/entrypoint \
|
||||||
|
&& chmod +x /usr/local/bin/entrypoint \
|
||||||
|
&& mv ./docker/vnc-start.sh /usr/local/bin/vnc-start \
|
||||||
|
&& chmod +x /usr/local/bin/vnc-start
|
||||||
|
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint"]
|
||||||
|
CMD ["node", "epic-games.js"]
|
||||||
15
docker/entrypoint.sh
Executable file
15
docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# 6000+SERVERNUM is the TCP port Xvfb is listening on:
|
||||||
|
# SERVERNUM=$(echo "$DISPLAY" | sed 's/:\([0-9][0-9]*\).*/\1/')
|
||||||
|
|
||||||
|
# Options passed directly to the Xvfb server:
|
||||||
|
# -ac disables host-based access control mechanisms
|
||||||
|
# −screen NUM WxHxD creates the screen and sets its width, height, and depth
|
||||||
|
Xvfb "$DISPLAY" -ac -screen 0 "${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH}" >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
if [ "$VNC_ENABLED" = true ]; then
|
||||||
|
vnc-start >/dev/null 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec tini -g -- "$@"
|
||||||
11
docker/vnc-start.sh
Executable file
11
docker/vnc-start.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Start VNC in a background process:
|
||||||
|
x11vnc -display "$DISPLAY" -forever -shared -rfbport "${VNC_PORT:-5900}" \
|
||||||
|
-passwd "${VNC_PASSWORD:-secret}" -bg
|
||||||
|
NOVNC_HOME=/usr/share/novnc
|
||||||
|
ln -s $NOVNC_HOME/vnc_auto.html $NOVNC_HOME/index.html
|
||||||
|
websockify -D --web "$NOVNC_HOME" "$NOVNC_PORT" "localhost:$VNC_PORT" &
|
||||||
|
|
||||||
|
# Execute the given command:
|
||||||
|
exec "$@"
|
||||||
|
|
@ -3,16 +3,19 @@ import path from 'path';
|
||||||
import { __dirname, stealth } from './util.js';
|
import { __dirname, stealth } from './util.js';
|
||||||
const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector
|
const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector
|
||||||
|
|
||||||
const URL_LOGIN = 'https://www.epicgames.com/login';
|
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
|
||||||
const URL_CLAIM = 'https://www.epicgames.com/store/en-US/free-games';
|
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
|
||||||
const TIMEOUT = 20 * 1000; // 20s, default is 30s
|
const TIMEOUT = 20 * 1000; // 20s, default is 30s
|
||||||
|
const SCREEN_WIDTH = Number(process.env.SCREEN_WIDTH) - 80 || 1280;
|
||||||
|
const SCREEN_HEIGHT = Number(process.env.SCREEN_HEIGHT) || 1280;
|
||||||
|
|
||||||
// https://playwright.dev/docs/auth#multi-factor-authentication
|
// https://playwright.dev/docs/auth#multi-factor-authentication
|
||||||
const context = await chromium.launchPersistentContext(path.resolve(__dirname, 'userDataDir'), {
|
const context = await chromium.launchPersistentContext(path.resolve(__dirname, 'userDataDir'), {
|
||||||
channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge
|
// chrome will not work in linux arm64, only chromium
|
||||||
|
// channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge
|
||||||
headless: false,
|
headless: false,
|
||||||
viewport: { width: 1280, height: 1280 },
|
viewport: { width: SCREEN_WIDTH, height: SCREEN_HEIGHT },
|
||||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated!
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated!
|
||||||
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
|
||||||
args: [ // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.', but flags below don't work.
|
args: [ // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.', but flags below don't work.
|
||||||
'--disable-session-crashed-bubble',
|
'--disable-session-crashed-bubble',
|
||||||
|
|
@ -35,15 +38,15 @@ const clickIfExists = async selector => {
|
||||||
await page.click(selector);
|
await page.click(selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
await page.goto(URL_CLAIM, {waitUntil: 'domcontentloaded'}); // default 'load' takes forever
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||||
// with persistent context the cookie message will only show up the first time, so we can't unconditionally wait for it - try to catch it or let the user click it.
|
// with persistent context the cookie message will only show up the first time, so we can't unconditionally wait for it - try to catch it or let the user click it.
|
||||||
await clickIfExists('button:has-text("Accept All Cookies")'); // to not waste screen space in --debug
|
await clickIfExists('button:has-text("Accept All Cookies")'); // to not waste screen space in --debug
|
||||||
while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { // TODO also check alternative for signed-in state
|
while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { // TODO also check alternative for signed-in state
|
||||||
console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page.");
|
console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page.");
|
||||||
context.setDefaultTimeout(0); // give user time to log in without timeout
|
context.setDefaultTimeout(0); // give user time to log in without timeout
|
||||||
await page.goto(URL_LOGIN, {waitUntil: 'domcontentloaded'});
|
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
||||||
// after login it just reloads the login page...
|
// after login it just reloads the login page...
|
||||||
await page.waitForNavigation({url: URL_CLAIM});
|
await page.waitForNavigation({ url: URL_CLAIM });
|
||||||
context.setDefaultTimeout(TIMEOUT);
|
context.setDefaultTimeout(TIMEOUT);
|
||||||
// process.exit(1);
|
// process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +57,7 @@ await page.waitForSelector(game_sel);
|
||||||
// const games = await page.$$(game_sel); // 'Element is not attached to the DOM' after navigation; had `for (const game of games) { await game.click(); ... }
|
// const games = await page.$$(game_sel); // 'Element is not attached to the DOM' after navigation; had `for (const game of games) { await game.click(); ... }
|
||||||
const n = await page.locator(game_sel).count();
|
const n = await page.locator(game_sel).count();
|
||||||
console.log('Number of free games:', n);
|
console.log('Number of free games:', n);
|
||||||
for (let i=1; i<=n; i++) {
|
for (let i = 1; i <= n; i++) {
|
||||||
await page.click(`:nth-match(${game_sel}, ${i})`);
|
await page.click(`:nth-match(${game_sel}, ${i})`);
|
||||||
const title = await page.locator('h1 div').first().innerText();
|
const title = await page.locator('h1 div').first().innerText();
|
||||||
console.log('Current free game:', title);
|
console.log('Current free game:', title);
|
||||||
|
|
@ -83,10 +86,7 @@ for (let i=1; i<=n; i++) {
|
||||||
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
|
// 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 Agree")');
|
||||||
try {
|
try {
|
||||||
await Promise.any([btnAgree.waitFor(), page.waitForSelector('text=Thank you for buying')]); // EU: wait for agree button, non-EU: potentially done
|
await Promise.any([btnAgree.waitFor().then(() => btnAgree.click()), page.waitForSelector('text=Thank you for buying')]); // EU: wait for agree button, non-EU: potentially done
|
||||||
// await clickIfExists('button:has-text("I Agree")', iframe); // default arg: FrameLocator is incompatible with Page and even Locator...
|
|
||||||
if (await btnAgree.count() > 0)
|
|
||||||
await btnAgree.click();
|
|
||||||
// TODO check for hcaptcha - the following is even true when no captcha is shown...
|
// TODO check for hcaptcha - the following is even true when no captcha is shown...
|
||||||
// if (await iframe.frameLocator('#talon_frame_checkout_free_prod').locator('text=Please complete a security check to continue').count() > 0) {
|
// if (await iframe.frameLocator('#talon_frame_checkout_free_prod').locator('text=Please complete a security check to continue').count() > 0) {
|
||||||
// console.error('Encountered hcaptcha. Giving up :(');
|
// console.error('Encountered hcaptcha. Giving up :(');
|
||||||
|
|
@ -105,8 +105,8 @@ for (let i=1; i<=n; i++) {
|
||||||
}
|
}
|
||||||
// await page.pause();
|
// await page.pause();
|
||||||
}
|
}
|
||||||
if (i<n) { // no need to go back if it's the last game
|
if (i < n) { // no need to go back if it's the last game
|
||||||
await page.goto(URL_CLAIM, {waitUntil: 'domcontentloaded'});
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForSelector(game_sel);
|
await page.waitForSelector(game_sel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2104
package-lock.json
generated
2104
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,9 +7,9 @@
|
||||||
"start": "node main.stealth"
|
"start": "node main.stealth"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.17.1",
|
"@playwright/test": "^1.20.1",
|
||||||
"playwright": "^1.17.1",
|
"playwright": "^1.20.1",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.9.0"
|
"puppeteer-extra-plugin-stealth": "^2.9.0"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue