Compare commits
No commits in common. "0a8fc8543976b2a090f415b311c15199a0cae55a" and "67afeead600a0e4d165ed52406faa8eceb48012e" have entirely different histories.
0a8fc85439
...
67afeead60
21 changed files with 252 additions and 1515 deletions
|
|
@ -1,36 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es2021: true,
|
|
||||||
es6: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-unused-vars': ['warn', {
|
|
||||||
varsIgnorePattern: '^_',
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
}],
|
|
||||||
'no-undef': 'error',
|
|
||||||
'@stylistic/js/comma-dangle': ['error', 'always-multiline'],
|
|
||||||
'@stylistic/js/arrow-parens': ['error', 'as-needed'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'@stylistic/js',
|
|
||||||
],
|
|
||||||
globals: {
|
|
||||||
screenshot: 'readonly',
|
|
||||||
cfg: 'readonly',
|
|
||||||
URL_CLAIM: 'readonly',
|
|
||||||
COOKIES_PATH: 'readonly',
|
|
||||||
BEARER_TOKEN_NAME: 'readonly',
|
|
||||||
notify: 'readonly',
|
|
||||||
authenticator: 'readonly',
|
|
||||||
prompt: 'readonly',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
"no-undef": "error"
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"cfg": "readonly",
|
|
||||||
"URL_CLAIM": "readonly",
|
|
||||||
"authenticator": "readonly",
|
|
||||||
"prompt": "readonly",
|
|
||||||
"notify": "readonly"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -4,60 +4,36 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- dev
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_TAG: ${{ github.ref == 'refs/heads/dev' && 'dev' || 'latest' }}
|
|
||||||
REPO_URL: https://git.sky-net.it
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
container:
|
|
||||||
image: node:20-alpine
|
|
||||||
steps:
|
steps:
|
||||||
- name: Manual Git Checkout
|
- name: Checkout
|
||||||
run: |
|
uses: actions/checkout@v4
|
||||||
apk add --no-cache git
|
- name: Setup Node.js
|
||||||
git init
|
uses: actions/setup-node@v4
|
||||||
git remote add origin ${{ env.REPO_URL }}/${{ github.repository }}.git
|
with:
|
||||||
git fetch --depth 1 origin ${{ github.ref }}
|
node-version: 20
|
||||||
git checkout FETCH_HEAD
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
sonar:
|
sonar:
|
||||||
needs: lint
|
needs: lint
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
container:
|
|
||||||
image: node:20-alpine
|
|
||||||
steps:
|
steps:
|
||||||
- name: Manual Git Checkout and Prepare
|
- name: Checkout
|
||||||
run: |
|
uses: actions/checkout@v4
|
||||||
apk add --no-cache git curl bash
|
with:
|
||||||
git init
|
fetch-depth: 0
|
||||||
git remote add origin ${{ env.REPO_URL }}/${{ github.repository }}.git
|
- name: Setup Node.js
|
||||||
git fetch --depth 1 origin ${{ github.ref }}
|
uses: actions/setup-node@v4
|
||||||
git checkout FETCH_HEAD
|
with:
|
||||||
|
node-version: 20
|
||||||
- name: Install Node.js and Sonar Scanner
|
|
||||||
run: |
|
|
||||||
npm install -g sonarqube-scanner
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y curl
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
||||||
apt-get install -y nodejs
|
|
||||||
|
|
||||||
- name: Install Sonar Scanner (npm)
|
- name: Install Sonar Scanner (npm)
|
||||||
run: npm install -g sonarqube-scanner
|
run: npm install -g sonarqube-scanner
|
||||||
|
|
||||||
- name: SonarQube Scan
|
- name: SonarQube Scan
|
||||||
env:
|
env:
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
|
|
@ -66,7 +42,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
WORKDIR=${GITHUB_WORKSPACE:-$PWD}
|
WORKDIR=${GITHUB_WORKSPACE:-$PWD}
|
||||||
HOST_URL=${SONAR_HOST_URL:?SONAR_HOST_URL secret not set}
|
HOST_URL=${SONAR_HOST_URL:?SONAR_HOST_URL secret not set}
|
||||||
BRANCH_NAME=${GITHUB_REF#refs/heads/}
|
|
||||||
PROJECT_KEY=${SONAR_PROJECT_KEY:-}
|
PROJECT_KEY=${SONAR_PROJECT_KEY:-}
|
||||||
if [ -z "$PROJECT_KEY" ] && [ -f sonar-project.properties ]; then
|
if [ -z "$PROJECT_KEY" ] && [ -f sonar-project.properties ]; then
|
||||||
PROJECT_KEY=$(grep -E '^sonar.projectKey=' sonar-project.properties | cut -d= -f2 | tr -d '\r')
|
PROJECT_KEY=$(grep -E '^sonar.projectKey=' sonar-project.properties | cut -d= -f2 | tr -d '\r')
|
||||||
|
|
@ -81,7 +56,7 @@ jobs:
|
||||||
echo "Sample files:"
|
echo "Sample files:"
|
||||||
find . -maxdepth 2 -type f | head -n 20
|
find . -maxdepth 2 -type f | head -n 20
|
||||||
echo "Running local sonar-scanner..."
|
echo "Running local sonar-scanner..."
|
||||||
set -- \
|
sonar-scanner \
|
||||||
-Dsonar.host.url="$HOST_URL" \
|
-Dsonar.host.url="$HOST_URL" \
|
||||||
-Dsonar.token="$SONAR_TOKEN" \
|
-Dsonar.token="$SONAR_TOKEN" \
|
||||||
-Dsonar.projectKey="$PROJECT_KEY" \
|
-Dsonar.projectKey="$PROJECT_KEY" \
|
||||||
|
|
@ -89,46 +64,24 @@ jobs:
|
||||||
-Dsonar.scm.disabled=true \
|
-Dsonar.scm.disabled=true \
|
||||||
-Dsonar.projectBaseDir="$WORKDIR"
|
-Dsonar.projectBaseDir="$WORKDIR"
|
||||||
|
|
||||||
if [ "${SONAR_ENABLE_BRANCH:-}" = "true" ]; then
|
|
||||||
set -- "$@" -Dsonar.branch.name="$BRANCH_NAME"
|
|
||||||
else
|
|
||||||
echo "Branch analysis disabled (requires SonarQube Developer Edition)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sonar-scanner "$@"
|
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
needs: [lint, sonar]
|
needs: [lint, sonar]
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Network Debugging
|
|
||||||
run: |
|
|
||||||
cat /etc/resolv.conf
|
|
||||||
cat /etc/hosts
|
|
||||||
ping -c 4 server
|
|
||||||
getent hosts server
|
|
||||||
|
|
||||||
- name: Manual Git Checkout
|
|
||||||
run: |
|
|
||||||
git init
|
|
||||||
git remote add origin ${{ env.REPO_URL }}/${{ github.repository }}.git
|
|
||||||
git fetch --depth 1 origin ${{ github.ref }}
|
|
||||||
git checkout FETCH_HEAD
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
run: echo "${{ secrets.REG_TOKEN }}" | docker login "${{ secrets.REGISTRY }}" -u "${{ secrets.REG_USER }}" --password-stdin
|
run: echo "${{ secrets.REG_TOKEN }}" | docker login "${{ secrets.REGISTRY }}" -u "${{ secrets.REG_USER }}" --password-stdin
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: |
|
run: |
|
||||||
docker buildx build --load \
|
docker buildx build --load \
|
||||||
-t "${{ secrets.REGISTRY_IMAGE }}:${{ env.IMAGE_TAG }}" .
|
-t "${{ secrets.REGISTRY_IMAGE }}:latest" .
|
||||||
|
|
||||||
- name: Push image
|
- name: Push image
|
||||||
run: |
|
run: |
|
||||||
docker push "${{ secrets.REGISTRY_IMAGE }}:${{ env.IMAGE_TAG }}"
|
docker push "${{ secrets.REGISTRY_IMAGE }}:latest"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
data/
|
data/
|
||||||
*.env
|
*.env
|
||||||
.continue
|
|
||||||
|
|
|
||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
### eslint style
|
|
||||||
{
|
{
|
||||||
// https://eslint.style/guide/faq#vs-code
|
// https://eslint.style/guide/faq#vs-code
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,8 @@ ENV DEPTH 24
|
||||||
# Show browser instead of running headless
|
# Show browser instead of running headless
|
||||||
ENV SHOW 1
|
ENV SHOW 1
|
||||||
|
|
||||||
|
USER fgc
|
||||||
|
|
||||||
# Script to setup display server & VNC is always executed.
|
# Script to setup display server & VNC is always executed.
|
||||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
# Default command to run. This is replaced by appending own command, e.g. `docker run ... node prime-gaming` to only run this script.
|
# Default command to run. This is replaced by appending own command, e.g. `docker run ... node prime-gaming` to only run this script.
|
||||||
|
|
|
||||||
49
README.md
49
README.md
|
|
@ -2,12 +2,12 @@ Free Games Claimer (Fork)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
[](https://sonata.cyber77.de/dashboard?id=free-games-claimer)
|
[](https://sonata.cyber77.de/dashboard?id=free-games-claimer)
|
||||||
- Optional notifications: `pip install apprise`
|
|
||||||
Automates claiming of free games for:
|
Automates claiming of free games for:
|
||||||
- Amazon Luna Gaming / Luna claims (including external stores like GOG, Epic Games, Legacy Games )
|
- Amazon Luna Gaming / Luna claims (including external stores like GOG, Epic Games, Legacy Games )
|
||||||
- GOG giveaways
|
- GOG giveaways
|
||||||
- Optional extras: Steam stats, AliExpress dailies (not implemated yet)
|
- Optional extras: Steam stats, AliExpress dailies (not implemated yet)
|
||||||
-p 6080:6080 \
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
- Docker or Podman (recommended), or Node.js ≥ 20 for local runs
|
- Docker or Podman (recommended), or Node.js ≥ 20 for local runs
|
||||||
|
|
@ -19,45 +19,35 @@ Quickstart (Docker Run)
|
||||||
```
|
```
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-p 6080:6080 \
|
-p 6080:6080 \
|
||||||
-v fgc-data:/fgc/data \
|
-v fgc:/fgc/data \
|
||||||
-v fgc-browser:/home/fgc/.cache/browser \
|
|
||||||
-v fgc-playwright:/home/fgc/.cache/ms-playwright \
|
|
||||||
-e SHOW=1 \
|
-e SHOW=1 \
|
||||||
git.sky-net.it/nocci/free-games-claimer:dev \
|
git.sky-net.it/nocci/free-games-claimer:latest \
|
||||||
bash -c "node prime-gaming; node gog; ./keep-alive.sh"
|
node prime-gaming.js
|
||||||
```
|
```
|
||||||
- Ports 6080/5900: noVNC/VNC (only needed with `SHOW=1`)
|
- Ports 6080/5900: noVNC/VNC (only needed with `SHOW=1`)
|
||||||
- Volumes persist profile + Playwright-Browser, damit Logins/Downloads bleiben.
|
- Data/configs are stored in volume `fgc` under `/fgc/data`
|
||||||
|
|
||||||
Docker Compose Example (persistent volumes)
|
Docker Compose Example
|
||||||
-------------------------------------------
|
----------------------
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
free-games-claimer:
|
fgc:
|
||||||
image: git.sky-net.it/nocci/free-games-claimer:dev
|
image: git.sky-net.it/nocci/free-games-claimer:latest
|
||||||
container_name: fgc
|
container_name: fgc
|
||||||
environment:
|
environment:
|
||||||
- SHOW=1 # show browser via VNC/noVNC
|
- SHOW=1 # show browser via VNC/noVNC
|
||||||
# - PG_EMAIL=...
|
# - PG_EMAIL=...
|
||||||
# - PG_PASSWORD=...
|
# - PG_PASSWORD=...
|
||||||
# - PG_OTPKEY=...
|
# - PG_OTPKEY=...
|
||||||
- BROWSER_DIR=/fgc/data/browser
|
|
||||||
- LOGIN_VISIBLE_TIMEOUT=20 # optional: faster login detection
|
|
||||||
- KEEP_ALIVE_SECONDS=86400 # optional: keep container alive after runs
|
|
||||||
volumes:
|
volumes:
|
||||||
- fgc-data:/fgc/data
|
- fgc:/fgc/data
|
||||||
- fgc-browser:/home/fgc/.cache/browser
|
|
||||||
- fgc-playwright:/home/fgc/.cache/ms-playwright
|
|
||||||
ports:
|
ports:
|
||||||
- "6080:6080" # noVNC
|
- "6080:6080" # noVNC
|
||||||
# - "5900:5900" # VNC optional
|
# - "5900:5900" # VNC optional
|
||||||
command: bash -c "node prime-gaming; node gog; ./keep-alive.sh"
|
command: bash -c "node epic-games; node prime-gaming; node gog"
|
||||||
volumes:
|
volumes:
|
||||||
fgc-data:
|
fgc:
|
||||||
fgc-browser:
|
|
||||||
fgc-playwright:
|
|
||||||
```
|
```
|
||||||
Hinweis: Das Image läuft auf `dev`; bei Bedarf `:latest` wählen.
|
|
||||||
|
|
||||||
Configuration (Environment Variables)
|
Configuration (Environment Variables)
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
@ -65,9 +55,6 @@ Common options:
|
||||||
- `SHOW=0/1` (0 = headless, 1 = UI)
|
- `SHOW=0/1` (0 = headless, 1 = UI)
|
||||||
- `WIDTH`, `HEIGHT` (browser size)
|
- `WIDTH`, `HEIGHT` (browser size)
|
||||||
- `TIMEOUT`, `LOGIN_TIMEOUT` (seconds)
|
- `TIMEOUT`, `LOGIN_TIMEOUT` (seconds)
|
||||||
- Epic: `EG_MODE=legacy|new` (legacy Playwright flow or neuer API-getriebener Claimer), `EG_PARENTALPIN`, `EG_EMAIL`, `EG_PASSWORD`, `EG_OTPKEY`
|
|
||||||
- Epic (new mode): Cookies werden unter `data/browser/epic-cookies.json` persistiert; OAuth Device Code Flow benötigt ggf. einmalige Freigabe im Browser.
|
|
||||||
- Falls Device-Code-Endpunkt nicht erreichbar ist (404/Bad Request), fällt der neue Modus automatisch auf manuellen Browser-Login zurück.
|
|
||||||
- Login: `EMAIL`, `PASSWORD` global; per store `EG_EMAIL`, `EG_PASSWORD`, `EG_OTPKEY`, `PG_EMAIL`, `PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`
|
- Login: `EMAIL`, `PASSWORD` global; per store `EG_EMAIL`, `EG_PASSWORD`, `EG_OTPKEY`, `PG_EMAIL`, `PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`
|
||||||
- Prime Gaming: `PG_REDEEM=1` (auto-redeem keys, experimental), `PG_CLAIMDLC=1`, `PG_TIMELEFT=<days>` to skip long-remaining offers
|
- Prime Gaming: `PG_REDEEM=1` (auto-redeem keys, experimental), `PG_CLAIMDLC=1`, `PG_TIMELEFT=<days>` to skip long-remaining offers
|
||||||
- Screenshots: `SCREENSHOTS_DIR` (default `data/screenshots`)
|
- Screenshots: `SCREENSHOTS_DIR` (default `data/screenshots`)
|
||||||
|
|
@ -79,9 +66,6 @@ Common options:
|
||||||
- Directories: `SCREENSHOTS_DIR`, `BROWSER_DIR`, `DATA_DIR` (prefix for data; default under `data/`)
|
- Directories: `SCREENSHOTS_DIR`, `BROWSER_DIR`, `DATA_DIR` (prefix for data; default under `data/`)
|
||||||
- VNC/noVNC: `VNC_PASSWORD` (for Docker entrypoint), `NOVNC_PORT`/`VNC_PORT` (Docker)
|
- VNC/noVNC: `VNC_PASSWORD` (for Docker entrypoint), `NOVNC_PORT`/`VNC_PORT` (Docker)
|
||||||
- General timeouts: `TIMEOUT` (per action), `LOGIN_TIMEOUT` (extra time for login)
|
- General timeouts: `TIMEOUT` (per action), `LOGIN_TIMEOUT` (extra time for login)
|
||||||
- Login detection: `LOGIN_VISIBLE_TIMEOUT` (ms) to abort sooner when login buttons not present
|
|
||||||
- Keep-alive: `KEEP_ALIVE_SECONDS` (default 86400) for `keep-alive.sh`
|
|
||||||
- Repo banner: `REPO_URL` for log output
|
|
||||||
|
|
||||||
You can place a `data/config.env`; it is loaded via dotenv and is overridden by explicitly set environment variables.
|
You can place a `data/config.env`; it is loaded via dotenv and is overridden by explicitly set environment variables.
|
||||||
|
|
||||||
|
|
@ -102,4 +86,3 @@ Persistence & Outputs
|
||||||
- Optional videos/HAR: `RECORD=1` → `data/record/`
|
- Optional videos/HAR: `RECORD=1` → `data/record/`
|
||||||
|
|
||||||
Tip: For captchas or first-time login, run with `SHOW=1` and log in once; cookies stay in the profile. Notifications via `NOTIFY` help surface errors (e.g., captcha, login).
|
Tip: For captchas or first-time login, run with `SHOW=1` and log in once; cookies stay in the profile. Notifications via `NOTIFY` help surface errors (e.g., captcha, login).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,8 @@
|
||||||
|
|
||||||
set -eo pipefail # exit on error, error on any fail in pipe (not just last cmd); add -x to print each cmd; see gist bash_strict_mode.md
|
set -eo pipefail # exit on error, error on any fail in pipe (not just last cmd); add -x to print each cmd; see gist bash_strict_mode.md
|
||||||
|
|
||||||
REPO_URL=${REPO_URL:-https://git.sky-net.it/nocci/free-games-claimer}
|
echo "Version: https://github.com/vogler/free-games-claimer/tree/${COMMIT}"
|
||||||
if [ -n "$COMMIT" ]; then
|
[ ! -z $BRANCH ] && [ $BRANCH != "main" ] && echo "Branch: ${BRANCH}"
|
||||||
echo "Version: ${REPO_URL}/tree/${COMMIT}"
|
|
||||||
else
|
|
||||||
echo "Version: ${REPO_URL}"
|
|
||||||
fi
|
|
||||||
[ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && echo "Branch: ${BRANCH}"
|
|
||||||
echo "Build: $NOW"
|
echo "Build: $NOW"
|
||||||
|
|
||||||
# Ensure writable data dir for fgc when host bind-mount is owned by root.
|
# Ensure writable data dir for fgc when host bind-mount is owned by root.
|
||||||
|
|
@ -22,38 +17,23 @@ fi
|
||||||
# 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 2>/dev/null || true
|
rm -f /fgc/data/browser/SingletonLock 2>/dev/null || true
|
||||||
|
|
||||||
# Firefox profile directory (persistent if writable; fallback to cache when bind-mount is read-only).
|
|
||||||
BROWSER_DIR=/fgc/data/browser
|
|
||||||
mkdir -p "$BROWSER_DIR" 2>/dev/null || true
|
|
||||||
if [ ! -w "$BROWSER_DIR" ]; then
|
|
||||||
echo "Warning: $BROWSER_DIR not writable; using fallback profile at /home/fgc/.cache/browser"
|
|
||||||
BROWSER_DIR=/home/fgc/.cache/browser
|
|
||||||
mkdir -p "$BROWSER_DIR" 2>/dev/null || true
|
|
||||||
chown 1000:1000 "$BROWSER_DIR" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
if [ ! -w "$BROWSER_DIR" ]; then
|
|
||||||
echo "Warning: $BROWSER_DIR not writable; using temp profile at /tmp/browser"
|
|
||||||
BROWSER_DIR=/tmp/browser
|
|
||||||
mkdir -p "$BROWSER_DIR"
|
|
||||||
chmod 777 "$BROWSER_DIR" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
# clean up stale firefox locks that can trigger "already running"
|
|
||||||
rm -f "$BROWSER_DIR"/parent.lock "$BROWSER_DIR"/lock "$BROWSER_DIR"/.parentlock 2>/dev/null || true
|
|
||||||
# Firefox preferences are stored in $BROWSER_DIR/pref.js and can be overridden by a file user.js
|
# 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.
|
# Since this file has to be in the volume (data/browser), we can't do this in Dockerfile.
|
||||||
|
mkdir -p /fgc/data/browser
|
||||||
|
# clean up stale firefox locks that can trigger "already running"
|
||||||
|
rm -f /fgc/data/browser/parent.lock /fgc/data/browser/lock /fgc/data/browser/.parentlock 2>/dev/null || true
|
||||||
# fix for 'Incorrect response' after solving a captcha correctly - https://github.com/vogler/free-games-claimer/issues/261#issuecomment-1868385830
|
# fix for 'Incorrect response' after solving a captcha correctly - https://github.com/vogler/free-games-claimer/issues/261#issuecomment-1868385830
|
||||||
# Only write the prefs file when the volume is writable (container runs as non-root).
|
# Only write the prefs file when the volume is writable (container runs as non-root).
|
||||||
if [ -w "$BROWSER_DIR" ] && { [ ! -e "$BROWSER_DIR/user.js" ] || [ -w "$BROWSER_DIR/user.js" ] || rm -f "$BROWSER_DIR/user.js" 2>/dev/null; }; then
|
if [ -w /fgc/data/browser ] && { [ ! -e /fgc/data/browser/user.js ] || [ -w /fgc/data/browser/user.js ] || rm -f /fgc/data/browser/user.js 2>/dev/null; }; then
|
||||||
cat << 'EOT' > "$BROWSER_DIR/user.js"
|
cat << 'EOT' > /fgc/data/browser/user.js
|
||||||
user_pref("privacy.resistFingerprinting", true);
|
user_pref("privacy.resistFingerprinting", true);
|
||||||
// user_pref("privacy.resistFingerprinting.letterboxing", true);
|
// user_pref("privacy.resistFingerprinting.letterboxing", true);
|
||||||
// user_pref("browser.contentblocking.category", "strict");
|
// user_pref("browser.contentblocking.category", "strict");
|
||||||
// user_pref("webgl.disabled", true);
|
// user_pref("webgl.disabled", true);
|
||||||
EOT
|
EOT
|
||||||
else
|
else
|
||||||
echo "Warning: $BROWSER_DIR not writable; skipping user.js creation."
|
echo "Warning: /fgc/data/browser not writable; skipping user.js creation."
|
||||||
fi
|
fi
|
||||||
export BROWSER_DIR
|
|
||||||
# TODO disable session restore message?
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import { firefox } from 'playwright-firefox';
|
|
||||||
import { authenticator } from 'otplib';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
||||||
import {
|
|
||||||
jsonDb,
|
|
||||||
datetime,
|
|
||||||
stealth,
|
|
||||||
filenamify,
|
|
||||||
prompt,
|
|
||||||
notify,
|
|
||||||
html_game_list,
|
|
||||||
handleSIGINT,
|
|
||||||
} from './src/util.js';
|
|
||||||
import { cfg } from './src/config.js';
|
|
||||||
|
|
||||||
|
|
||||||
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
|
|
||||||
const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json');
|
|
||||||
const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN';
|
|
||||||
|
|
||||||
// Screenshot Helper Function
|
|
||||||
|
|
||||||
|
|
||||||
// Fetch Free Games from API
|
|
||||||
const fetchFreeGamesAPI = async () => {
|
|
||||||
const resp = await axios.get('https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions', {
|
|
||||||
params: { locale: 'en-US', country: 'US', allowCountries: 'US,DE,AT,CH,GB' },
|
|
||||||
});
|
|
||||||
return resp.data?.Catalog?.searchStore?.elements
|
|
||||||
?.filter(g => g.promotions?.promotionalOffers?.[0])
|
|
||||||
?.map(g => {
|
|
||||||
const offer = g.promotions.promotionalOffers[0].promotionalOffers[0];
|
|
||||||
const mapping = g.catalogNs?.mappings?.[0];
|
|
||||||
return {
|
|
||||||
title: g.title,
|
|
||||||
namespace: mapping?.id || g.productSlug,
|
|
||||||
pageSlug: mapping?.pageSlug || g.urlSlug,
|
|
||||||
offerId: offer?.offerId,
|
|
||||||
};
|
|
||||||
}) || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll for OAuth tokens
|
|
||||||
const pollForTokens = async (deviceCode, maxAttempts = 30) => {
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('https://api.epicgames.dev/epic/oauth/token', {
|
|
||||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
||||||
device_code: deviceCode,
|
|
||||||
client_id: '34a02cf8f4414e29b159cdd02e6184bd',
|
|
||||||
});
|
|
||||||
if (response.data?.access_token) {
|
|
||||||
console.log('✅ OAuth successful');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.data?.error === 'authorization_pending') {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('OAuth timeout');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exchange token for cookies
|
|
||||||
const exchangeTokenForCookies = async accessToken => {
|
|
||||||
const response = await axios.get('https://store.epicgames.com/', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const cookies = response.headers['set-cookie']?.map(cookie => {
|
|
||||||
const [name, value] = cookie.split(';')[0].split('=');
|
|
||||||
return { name, value, domain: '.epicgames.com', path: '/' };
|
|
||||||
}) || [];
|
|
||||||
cookies.push({ name: BEARER_TOKEN_NAME, value: accessToken, domain: '.epicgames.com', path: '/' });
|
|
||||||
return cookies;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get valid authentication
|
|
||||||
const getValidAuth = async ({ otpKey, reuseCookies, cookiesPath }) => {
|
|
||||||
if (reuseCookies && existsSync(cookiesPath)) {
|
|
||||||
const cookies = JSON.parse(readFileSync(cookiesPath, 'utf8'));
|
|
||||||
const bearerCookie = cookies.find(c => c.name === BEARER_TOKEN_NAME);
|
|
||||||
if (bearerCookie?.value) {
|
|
||||||
console.log('🔄 Reusing existing bearer token from cookies');
|
|
||||||
return { bearerToken: bearerCookie.value, cookies };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔐 Starting fresh OAuth device flow (manual approval required)...');
|
|
||||||
let deviceResponse;
|
|
||||||
|
|
||||||
try {
|
|
||||||
deviceResponse = await axios.post('https://api.epicgames.dev/epic/oauth/deviceCode', {
|
|
||||||
client_id: '34a02cf8f4414e29b159cdd02e6184bd',
|
|
||||||
scope: 'account.basicprofile account.userentitlements',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Device code flow failed (fallback to manual login):', error.response?.status || error.message);
|
|
||||||
return { bearerToken: null, cookies: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { device_code, user_code, verification_uri_complete } = deviceResponse.data;
|
|
||||||
console.log(`📱 Open: ${verification_uri_complete}`);
|
|
||||||
console.log(`💳 Code: ${user_code}`);
|
|
||||||
|
|
||||||
const tokens = await pollForTokens(device_code);
|
|
||||||
|
|
||||||
if (otpKey) {
|
|
||||||
const totpCode = authenticator.generate(otpKey);
|
|
||||||
console.log(`🔑 TOTP Code (generated): ${totpCode}`);
|
|
||||||
try {
|
|
||||||
const refreshed = await axios.post('https://api.epicgames.dev/epic/oauth/token', {
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: tokens.refresh_token,
|
|
||||||
code_verifier: totpCode,
|
|
||||||
});
|
|
||||||
tokens.access_token = refreshed.data.access_token;
|
|
||||||
} catch {
|
|
||||||
// Ignore if refresh fails; use original token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookies = await exchangeTokenForCookies(tokens.access_token);
|
|
||||||
writeFileSync(cookiesPath, JSON.stringify(cookies, null, 2));
|
|
||||||
console.log('💾 Cookies saved to', cookiesPath);
|
|
||||||
return { bearerToken: tokens.access_token, cookies };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure user is logged in
|
|
||||||
const ensureLoggedIn = async (page, context) => {
|
|
||||||
const isLoggedIn = async () => await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
|
||||||
|
|
||||||
const attemptAutoLogin = async () => {
|
|
||||||
if (!cfg.eg_email || !cfg.eg_password) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await page.goto('https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM, {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: cfg.login_timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first();
|
|
||||||
const passwordField = page.locator('input[name="password"], input#password').first();
|
|
||||||
const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first();
|
|
||||||
|
|
||||||
// Step 1: Email + continue
|
|
||||||
if (await emailField.count() > 0) {
|
|
||||||
await emailField.fill(cfg.eg_email);
|
|
||||||
await continueBtn.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Password + submit
|
|
||||||
await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
|
|
||||||
await passwordField.fill(cfg.eg_password);
|
|
||||||
|
|
||||||
const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
|
|
||||||
if (await rememberMe.count() > 0) await rememberMe.check();
|
|
||||||
await continueBtn.click();
|
|
||||||
|
|
||||||
// MFA step
|
|
||||||
try {
|
|
||||||
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
|
||||||
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!',
|
|
||||||
});
|
|
||||||
|
|
||||||
const codeInputs = page.locator('input[name^="code-input"]');
|
|
||||||
if (await codeInputs.count() > 0) {
|
|
||||||
const digits = otp.toString().split('');
|
|
||||||
for (let i = 0; i < digits.length; i++) {
|
|
||||||
const input = codeInputs.nth(i);
|
|
||||||
await input.fill(digits[i]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
|
||||||
}
|
|
||||||
await continueBtn.click();
|
|
||||||
} catch {
|
|
||||||
// No MFA
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForURL('**/free-games', { timeout: cfg.login_timeout });
|
|
||||||
return await isLoggedIn();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Auto login failed:', err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChallenge = async () => {
|
|
||||||
const cfFrame = page.locator('iframe[title*="Cloudflare"], iframe[src*="challenges"]');
|
|
||||||
const cfText = page.locator('text=Verify you are human');
|
|
||||||
return await cfFrame.count() > 0 || await cfText.count() > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
let loginAttempts = 0;
|
|
||||||
const MAX_LOGIN_ATTEMPTS = 3;
|
|
||||||
|
|
||||||
while (!await isLoggedIn() && loginAttempts < MAX_LOGIN_ATTEMPTS) {
|
|
||||||
loginAttempts++;
|
|
||||||
console.error(`Not signed in (Attempt ${loginAttempts}). Trying automatic login, otherwise please login in the browser.`);
|
|
||||||
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout);
|
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
|
||||||
|
|
||||||
if (await isChallenge()) {
|
|
||||||
console.warn('Cloudflare challenge detected. Solve the captcha in the browser (no automation).');
|
|
||||||
await notify('epic-games (new): Cloudflare challenge, please solve manually in browser.');
|
|
||||||
await page.waitForTimeout(cfg.login_timeout);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logged = await attemptAutoLogin();
|
|
||||||
if (logged) break;
|
|
||||||
|
|
||||||
console.log('Waiting for manual login in the browser (cookies might be invalid).');
|
|
||||||
await notify('epic-games (new): please login in browser; cookies invalid or expired.');
|
|
||||||
|
|
||||||
if (cfg.headless) {
|
|
||||||
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
|
||||||
await context.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(cfg.login_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
|
|
||||||
console.error('Maximum login attempts reached. Exiting.');
|
|
||||||
await context.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await page.locator('egs-navigation').getAttribute('displayname');
|
|
||||||
console.log(`Signed in as ${user}`);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Claim game function
|
|
||||||
const claimGame = async (page, game) => {
|
|
||||||
const purchaseUrl = `https://store.epicgames.com/${game.pageSlug}`;
|
|
||||||
console.log(`🎮 ${game.title} → ${purchaseUrl}`);
|
|
||||||
const notify_game = { title: game.title, url: purchaseUrl, status: 'failed' };
|
|
||||||
|
|
||||||
await page.goto(purchaseUrl, { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"]').first();
|
|
||||||
await purchaseBtn.waitFor({ timeout: cfg.timeout });
|
|
||||||
const btnText = (await purchaseBtn.textContent() || '').toLowerCase();
|
|
||||||
|
|
||||||
if (btnText.includes('library') || btnText.includes('owned')) {
|
|
||||||
notify_game.status = 'existed';
|
|
||||||
return notify_game;
|
|
||||||
}
|
|
||||||
if (cfg.dryrun) {
|
|
||||||
notify_game.status = 'skipped';
|
|
||||||
return notify_game;
|
|
||||||
}
|
|
||||||
|
|
||||||
await purchaseBtn.click({ delay: 50 });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('#webPurchaseContainer iframe', { timeout: 15000 });
|
|
||||||
const iframe = page.frameLocator('#webPurchaseContainer iframe');
|
|
||||||
|
|
||||||
if (cfg.eg_parentalpin) {
|
|
||||||
try {
|
|
||||||
await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 });
|
|
||||||
await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin);
|
|
||||||
await iframe.locator('button:has-text("Continue")').click({ delay: 11 });
|
|
||||||
} catch {
|
|
||||||
// no PIN needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
|
|
||||||
try {
|
|
||||||
await iframe.locator('button:has-text("I Accept")').click({ timeout: 5000 });
|
|
||||||
} catch {
|
|
||||||
// not required
|
|
||||||
}
|
|
||||||
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached', timeout: cfg.timeout });
|
|
||||||
notify_game.status = 'claimed';
|
|
||||||
} catch (e) {
|
|
||||||
notify_game.status = 'failed';
|
|
||||||
const screenshotPath = path.resolve(cfg.dir.screenshots, 'epic-games', 'failed', `${game.offerId || game.pageSlug}_${filenamify(datetime())}.png`);
|
|
||||||
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => { });
|
|
||||||
console.error(' Failed to claim:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return notify_game;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main function to claim Epic Games
|
|
||||||
export const claimEpicGamesNew = async () => {
|
|
||||||
console.log('Starting Epic Games claimer (new mode, cookies + API)');
|
|
||||||
const db = await jsonDb('epic-games.json', {});
|
|
||||||
const notify_games = [];
|
|
||||||
|
|
||||||
const freeGames = await fetchFreeGamesAPI();
|
|
||||||
console.log('Free games via API:', freeGames.map(g => g.pageSlug));
|
|
||||||
|
|
||||||
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; rv:127.0) Gecko/20100101 Firefox/127.0',
|
|
||||||
locale: 'en-US',
|
|
||||||
recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined,
|
|
||||||
recordHar: cfg.record ? { path: `data/record/eg-${filenamify(datetime())}.har` } : undefined,
|
|
||||||
handleSIGINT: false,
|
|
||||||
});
|
|
||||||
handleSIGINT(context);
|
|
||||||
await stealth(context);
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
|
|
||||||
const page = context.pages().length ? context.pages()[0] : await context.newPage();
|
|
||||||
await page.setViewportSize({ width: cfg.width, height: cfg.height });
|
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auth = await getValidAuth({
|
|
||||||
email: cfg.eg_email,
|
|
||||||
password: cfg.eg_password,
|
|
||||||
otpKey: cfg.eg_otpkey,
|
|
||||||
reuseCookies: true,
|
|
||||||
cookiesPath: COOKIES_PATH,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (auth.cookies?.length) {
|
|
||||||
await context.addCookies(auth.cookies);
|
|
||||||
console.log('✅ Cookies loaded:', auth.cookies.length);
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ No cookies loaded; using manual login via browser.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
|
||||||
user = await ensureLoggedIn(page, context);
|
|
||||||
db.data[user] ||= {};
|
|
||||||
|
|
||||||
for (const game of freeGames) {
|
|
||||||
const result = await claimGame(page, game);
|
|
||||||
notify_games.push(result);
|
|
||||||
db.data[user][game.offerId || game.pageSlug] = {
|
|
||||||
title: game.title,
|
|
||||||
time: datetime(),
|
|
||||||
url: `https://store.epicgames.com/${game.pageSlug}`,
|
|
||||||
status: result.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFileSync(COOKIES_PATH, JSON.stringify(await context.cookies(), null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
process.exitCode ||= 1;
|
|
||||||
console.error('--- Exception (new epic):');
|
|
||||||
console.error(error);
|
|
||||||
if (error.message && process.exitCode !== 130) notify(`epic-games (new) failed: ${error.message.split('\\n')[0]}`);
|
|
||||||
} finally {
|
|
||||||
await db.write();
|
|
||||||
if (notify_games.filter(g => g.status === 'claimed' || g.status === 'failed').length) {
|
|
||||||
notify(`epic-games (new ${user || 'unknown'}):<br>${html_game_list(notify_games)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cfg.debug && context) {
|
|
||||||
console.log(JSON.stringify(await context.cookies(), null, 2));
|
|
||||||
}
|
|
||||||
await context.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default claimEpicGamesNew;
|
|
||||||
|
|
||||||
520
epic-games.js
520
epic-games.js
|
|
@ -1,26 +1,17 @@
|
||||||
import { firefox } from 'playwright-firefox';
|
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
|
||||||
import { authenticator } from 'otplib';
|
import { authenticator } from 'otplib';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { existsSync, writeFileSync, appendFileSync } from 'node:fs';
|
import { existsSync, writeFileSync, appendFileSync } from 'node:fs';
|
||||||
import { jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
|
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
|
||||||
import { cfg } from './src/config.js';
|
import { cfg } from './src/config.js';
|
||||||
import { EPIC_CLIENT_ID, GRAPHQL_ENDPOINT, FREE_GAMES_PROMOTIONS_ENDPOINT, STORE_HOMEPAGE_EN, EPIC_PURCHASE_ENDPOINT, ID_LOGIN_ENDPOINT } from './src/constants.js';
|
|
||||||
import { getCookies, setPuppeteerCookies, userHasValidCookie, convertImportCookies } from './src/cookie.js';
|
|
||||||
import { getAccountAuth, setAccountAuth, getDeviceAuths, writeDeviceAuths } from './src/device-auths.js';
|
|
||||||
|
|
||||||
const screenshot = (...a) => path.resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
||||||
|
|
||||||
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
|
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
|
||||||
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
|
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
|
||||||
|
|
||||||
console.log(datetime(), 'started checking epic-games (GraphQL API mode)');
|
console.log(datetime(), 'started checking epic-games');
|
||||||
|
|
||||||
if (cfg.eg_mode === 'new') {
|
|
||||||
const { claimEpicGamesNew } = await import('./epic-claimer-new.js');
|
|
||||||
await claimEpicGamesNew();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = await jsonDb('epic-games.json', {});
|
const db = await jsonDb('epic-games.json', {});
|
||||||
|
|
||||||
|
|
@ -29,7 +20,7 @@ if (cfg.time) console.time('startup');
|
||||||
const browserPrefs = path.join(cfg.dir.browser, 'prefs.js');
|
const browserPrefs = path.join(cfg.dir.browser, 'prefs.js');
|
||||||
if (existsSync(browserPrefs)) {
|
if (existsSync(browserPrefs)) {
|
||||||
console.log('Adding webgl.disabled to', browserPrefs);
|
console.log('Adding webgl.disabled to', browserPrefs);
|
||||||
appendFileSync(browserPrefs, 'user_pref("webgl.disabled", true);');
|
appendFileSync(browserPrefs, 'user_pref("webgl.disabled", true);'); // apparently Firefox removes duplicates (and sorts), so no problem appending every time
|
||||||
} else {
|
} else {
|
||||||
console.log(browserPrefs, 'does not exist yet, will patch it on next run. Restart the script if you get a captcha.');
|
console.log(browserPrefs, 'does not exist yet, will patch it on next run. Restart the script if you get a captcha.');
|
||||||
}
|
}
|
||||||
|
|
@ -38,25 +29,28 @@ if (existsSync(browserPrefs)) {
|
||||||
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
|
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 },
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0',
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // Windows UA avoids "device not supported"; update when browser version changes
|
||||||
locale: 'en-US',
|
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,
|
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,
|
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,
|
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
|
||||||
args: [],
|
// user settings for firefox have to be put in $BROWSER_DIR/user.js
|
||||||
|
args: [], // https://wiki.mozilla.org/Firefox/CommandLineOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSIGINT(context);
|
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);
|
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();
|
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
|
||||||
await page.setViewportSize({ width: cfg.width, height: cfg.height });
|
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
|
||||||
|
|
||||||
// some debug info about the page
|
// some debug info about the page (screen dimensions, user agent)
|
||||||
if (cfg.debug) {
|
if (cfg.debug) {
|
||||||
|
/* global window, navigator */
|
||||||
const debugInfo = await page.evaluate(() => {
|
const debugInfo = await page.evaluate(() => {
|
||||||
const { width, height, availWidth, availHeight } = window.screen;
|
const { width, height, availWidth, availHeight } = window.screen;
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,8 +60,8 @@ if (cfg.debug) {
|
||||||
});
|
});
|
||||||
console.debug(debugInfo);
|
console.debug(debugInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.debug_network) {
|
if (cfg.debug_network) {
|
||||||
|
// const filter = _ => true;
|
||||||
const filter = r => r.url().includes('store.epicgames.com');
|
const filter = r => r.url().includes('store.epicgames.com');
|
||||||
page.on('request', request => filter(request) && console.log('>>', request.method(), request.url()));
|
page.on('request', request => filter(request) && console.log('>>', request.method(), request.url()));
|
||||||
page.on('response', response => filter(response) && console.log('<<', response.status(), response.url()));
|
page.on('response', response => filter(response) && console.log('<<', response.status(), response.url()));
|
||||||
|
|
@ -76,266 +70,61 @@ if (cfg.debug_network) {
|
||||||
const notify_games = [];
|
const notify_games = [];
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
// GraphQL query for free games
|
|
||||||
const FREE_GAMES_QUERY = {
|
|
||||||
operationName: 'searchStoreQuery',
|
|
||||||
variables: {
|
|
||||||
allowCountries: 'US',
|
|
||||||
category: 'games/edition/base|software/edition/base|editors|bundles/games',
|
|
||||||
count: 1000,
|
|
||||||
country: 'US',
|
|
||||||
sortBy: 'relevancy',
|
|
||||||
sortDir: 'DESC',
|
|
||||||
start: 0,
|
|
||||||
withPrice: true,
|
|
||||||
},
|
|
||||||
extensions: {
|
|
||||||
persistedQuery: {
|
|
||||||
version: 1,
|
|
||||||
sha256Hash: '7d58e12d9dd8cb14c84a3ff18d360bf9f0caa96bf218f2c5fda68ba88d68a437',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate login redirect URL
|
|
||||||
const generateLoginRedirect = (redirectUrl) => {
|
|
||||||
const loginRedirectUrl = new URL(ID_LOGIN_ENDPOINT);
|
|
||||||
loginRedirectUrl.searchParams.set('noHostRedirect', 'true');
|
|
||||||
loginRedirectUrl.searchParams.set('redirectUrl', redirectUrl);
|
|
||||||
loginRedirectUrl.searchParams.set('client_id', EPIC_CLIENT_ID);
|
|
||||||
return loginRedirectUrl.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate checkout URL with login redirect
|
|
||||||
const generateCheckoutUrl = (offers) => {
|
|
||||||
const offersParams = offers
|
|
||||||
.map((offer) => `&offers=1-${offer.offerNamespace}-${offer.offerId}`)
|
|
||||||
.join('');
|
|
||||||
const checkoutUrl = `${EPIC_PURCHASE_ENDPOINT}?highlightColor=0078f2${offersParams}&orderId&purchaseToken&showNavigation=true`;
|
|
||||||
return generateLoginRedirect(checkoutUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get free games from GraphQL API
|
|
||||||
const getFreeGamesFromGraphQL = async () => {
|
|
||||||
const items = [];
|
|
||||||
let start = 0;
|
|
||||||
const pageLimit = 1000;
|
|
||||||
|
|
||||||
do {
|
|
||||||
const response = await page.evaluate(async (query, startOffset) => {
|
|
||||||
const variables = { ...query.variables, start: startOffset };
|
|
||||||
const resp = await fetch(GRAPHQL_ENDPOINT, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
operationName: query.operationName,
|
|
||||||
variables: JSON.stringify(variables),
|
|
||||||
extensions: JSON.stringify(query.extensions),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
return await resp.json();
|
|
||||||
}, [FREE_GAMES_QUERY, start]);
|
|
||||||
|
|
||||||
const elements = response.data?.Catalog?.searchStore?.elements;
|
|
||||||
if (!elements) break;
|
|
||||||
|
|
||||||
items.push(...elements);
|
|
||||||
start += pageLimit;
|
|
||||||
} while (items.length < pageLimit);
|
|
||||||
|
|
||||||
// Filter free games
|
|
||||||
const freeGames = items.filter(game =>
|
|
||||||
game.price?.totalPrice?.discountPrice === 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Deduplicate by productSlug
|
|
||||||
const uniqueGames = new Map();
|
|
||||||
for (const game of freeGames) {
|
|
||||||
if (!uniqueGames.has(game.productSlug)) {
|
|
||||||
uniqueGames.set(game.productSlug, game);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(uniqueGames.values()).map(game => ({
|
|
||||||
offerId: game.id,
|
|
||||||
offerNamespace: game.namespace,
|
|
||||||
productName: game.title,
|
|
||||||
productSlug: game.productSlug || game.urlSlug,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get free games from promotions API (weekly free games)
|
|
||||||
const getFreeGamesFromPromotions = async () => {
|
|
||||||
const response = await page.evaluate(async () => {
|
|
||||||
const resp = await fetch(FREE_GAMES_PROMOTIONS_ENDPOINT + '?locale=en-US&country=US&allowCountries=US');
|
|
||||||
return await resp.json();
|
|
||||||
});
|
|
||||||
|
|
||||||
const nowDate = new Date();
|
|
||||||
const elements = response.data?.Catalog?.searchStore?.elements || [];
|
|
||||||
|
|
||||||
return elements.filter(offer => {
|
|
||||||
if (!offer.promotions) return false;
|
|
||||||
|
|
||||||
return offer.promotions.promotionalOffers.some(innerOffers =>
|
|
||||||
innerOffers.promotionalOffers.some(pOffer => {
|
|
||||||
const startDate = new Date(pOffer.startDate);
|
|
||||||
const endDate = new Date(pOffer.endDate);
|
|
||||||
const isFree = pOffer.discountSetting?.discountPercentage === 0;
|
|
||||||
return startDate <= nowDate && nowDate <= endDate && isFree;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}).map(game => ({
|
|
||||||
offerId: game.id,
|
|
||||||
offerNamespace: game.namespace,
|
|
||||||
productName: game.title,
|
|
||||||
productSlug: game.productSlug || game.urlSlug,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all free games
|
|
||||||
const getAllFreeGames = async () => {
|
|
||||||
try {
|
|
||||||
const weeklyGames = await getFreeGamesFromPromotions();
|
|
||||||
console.log('Found', weeklyGames.length, 'weekly free games');
|
|
||||||
return weeklyGames;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to get weekly free games:', e.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login with device auth - attempts to use stored auth token
|
|
||||||
const loginWithDeviceAuth = async () => {
|
|
||||||
const deviceAuth = await getAccountAuth(cfg.eg_email || 'default');
|
|
||||||
|
|
||||||
if (deviceAuth && deviceAuth.access_token) {
|
|
||||||
console.log('Using stored device auth');
|
|
||||||
|
|
||||||
// Set the bearer token cookie for authentication
|
|
||||||
const bearerCookie = /** @type {import('playwright-firefox').Cookie} */ ({
|
|
||||||
name: 'EPIC_BEARER_TOKEN',
|
|
||||||
value: deviceAuth.access_token,
|
|
||||||
expires: new Date(deviceAuth.expires_at).getTime() / 1000,
|
|
||||||
domain: '.epicgames.com',
|
|
||||||
path: '/',
|
|
||||||
secure: true,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'Lax',
|
|
||||||
});
|
|
||||||
|
|
||||||
await context.addCookies([bearerCookie]);
|
|
||||||
|
|
||||||
// Visit store to get session cookies
|
|
||||||
await page.goto(STORE_HOMEPAGE_EN, { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Check if login worked
|
|
||||||
const isLoggedIn = await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
|
||||||
if (isLoggedIn) {
|
|
||||||
console.log('Successfully logged in with device auth');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exchange token for cookies (alternative method)
|
|
||||||
const exchangeTokenForCookies = async (accessToken) => {
|
|
||||||
try {
|
|
||||||
const cookies = await page.evaluate(async (token) => {
|
|
||||||
const resp = await fetch('https://store.epicgames.com/', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return await resp.headers.get('set-cookie');
|
|
||||||
}, accessToken);
|
|
||||||
|
|
||||||
return cookies;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save device auth
|
|
||||||
const saveDeviceAuth = async (accessToken, refreshToken, expiresAt) => {
|
|
||||||
const deviceAuth = {
|
|
||||||
access_token: accessToken,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
expires_at: expiresAt,
|
|
||||||
expires_in: 86400,
|
|
||||||
token_type: 'bearer',
|
|
||||||
account_id: 'unknown',
|
|
||||||
client_id: EPIC_CLIENT_ID,
|
|
||||||
internal_client: true,
|
|
||||||
client_service: 'account',
|
|
||||||
displayName: 'User',
|
|
||||||
app: 'epic-games',
|
|
||||||
in_app_id: 'unknown',
|
|
||||||
product_id: 'unknown',
|
|
||||||
refresh_expires: 604800,
|
|
||||||
refresh_expires_at: new Date(Date.now() + 604800000).toISOString(),
|
|
||||||
application_id: 'unknown',
|
|
||||||
};
|
|
||||||
|
|
||||||
await setAccountAuth(cfg.eg_email || 'default', deviceAuth);
|
|
||||||
console.log('Device auth saved');
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' },
|
{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' }, // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
|
||||||
{ name: 'HasAcceptedAgeGates', value: 'USK:9007199254740991,general:18,EPIC SUGGESTED RATING:18', domain: 'store.epicgames.com', path: '/' },
|
{ name: 'HasAcceptedAgeGates', value: 'USK:9007199254740991,general:18,EPIC SUGGESTED RATING:18', domain: 'store.epicgames.com', path: '/' }, // gets rid of 'To continue, please provide your date of birth', https://github.com/vogler/free-games-claimer/issues/275, USK number doesn't seem to matter, cookie from 'Fallout 3: Game of the Year Edition'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // 'domcontentloaded' faster than default 'load' https://playwright.dev/docs/api/class-page#page-goto
|
||||||
|
|
||||||
if (cfg.time) console.timeEnd('startup');
|
if (cfg.time) console.timeEnd('startup');
|
||||||
if (cfg.time) console.time('login');
|
if (cfg.time) console.time('login');
|
||||||
|
|
||||||
// Try device auth first
|
|
||||||
const deviceAuthLoginSuccess = await loginWithDeviceAuth();
|
|
||||||
|
|
||||||
// If device auth failed, try regular login
|
|
||||||
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
|
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
|
||||||
console.error('Not signed in. Please login in the browser or here in the terminal.');
|
console.error('Not signed in anymore. Please login in the browser or here in the terminal.');
|
||||||
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
|
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout);
|
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!`);
|
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
|
||||||
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
if (cfg.eg_email && cfg.eg_password) console.info('Using email and password from environment.');
|
if (cfg.eg_email && cfg.eg_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).');
|
else console.info('Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).');
|
||||||
|
|
||||||
const notifyBrowserLogin = async () => {
|
const notifyBrowserLogin = async () => {
|
||||||
console.log('Waiting for you to login in the browser.');
|
console.log('Waiting for you to login in the browser.');
|
||||||
await notify('epic-games: no longer signed in and not enough options set for automatic login.');
|
await notify('epic-games: no longer signed in and not enough options set for automatic login.');
|
||||||
if (cfg.headless) {
|
if (cfg.headless) {
|
||||||
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
||||||
await context.close();
|
await context.close(); // finishes potential recording
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCaptcha = await page.locator('.h_captcha_challenge iframe, text=Incorrect response').count() > 0;
|
|
||||||
if (hasCaptcha) {
|
|
||||||
console.warn('Captcha/Incorrect response detected. Please solve manually in the browser.');
|
|
||||||
await notify('epic-games: captcha encountered; please solve manually in browser.');
|
|
||||||
await page.waitForTimeout(cfg.login_timeout);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = cfg.eg_email || await prompt({ message: 'Enter email' });
|
const email = cfg.eg_email || await prompt({ message: 'Enter email' });
|
||||||
if (email) {
|
if (email) {
|
||||||
|
const watchCaptchaChallenge = async () => {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('.h_captcha_challenge iframe', { timeout: 15000 });
|
||||||
|
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.');
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const watchCaptchaIncorrect = async () => {
|
||||||
|
try {
|
||||||
|
await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 });
|
||||||
|
console.error('Incorrect response for captcha!');
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watchCaptchaChallenge();
|
||||||
|
watchCaptchaIncorrect();
|
||||||
await page.fill('#email', email);
|
await page.fill('#email', email);
|
||||||
const password = cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' });
|
const password = cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' });
|
||||||
if (password) {
|
if (password) {
|
||||||
await page.fill('#password', password);
|
await page.fill('#password', password);
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
} else await notifyBrowserLogin();
|
} else await notifyBrowserLogin();
|
||||||
|
|
||||||
const error = page.locator('#form-error-message');
|
const error = page.locator('#form-error-message');
|
||||||
const watchLoginError = async () => {
|
const watchLoginError = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -346,74 +135,58 @@ try {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const watchMfaStep = async () => {
|
const watchMfaStep = async () => {
|
||||||
try {
|
try {
|
||||||
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
||||||
console.log('Enter the security code to continue');
|
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 ...');
|
||||||
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!' });
|
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.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watchLoginError();
|
watchLoginError();
|
||||||
watchMfaStep();
|
watchMfaStep();
|
||||||
} else await notifyBrowserLogin();
|
} else await notifyBrowserLogin();
|
||||||
|
|
||||||
await page.waitForURL(URL_CLAIM);
|
await page.waitForURL(URL_CLAIM);
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
}
|
}
|
||||||
|
user = await page.locator('egs-navigation').getAttribute('displayname'); // 'null' if !isloggedin
|
||||||
user = await page.locator('egs-navigation').getAttribute('displayname');
|
|
||||||
console.log(`Signed in as ${user}`);
|
console.log(`Signed in as ${user}`);
|
||||||
db.data[user] ||= {};
|
db.data[user] ||= {};
|
||||||
|
|
||||||
if (cfg.time) console.timeEnd('login');
|
if (cfg.time) console.timeEnd('login');
|
||||||
if (cfg.time) console.time('claim all games');
|
if (cfg.time) console.time('claim all games');
|
||||||
|
|
||||||
// Get free games
|
// Detect free games
|
||||||
const freeGames = await getAllFreeGames();
|
const game_loc = page.locator('a:has(span:text-is("Free Now"))');
|
||||||
console.log('Free games:', freeGames.map(g => g.productName));
|
await game_loc.last().waitFor().catch(_ => {
|
||||||
|
// rarely there are no free games available -> catch Timeout
|
||||||
// Generate checkout link for all free games (available for all games)
|
// waiting for timeout; alternative would be waiting for "coming soon"
|
||||||
const checkoutUrl = freeGames.length > 0 ? generateCheckoutUrl(freeGames) : null;
|
// see https://github.com/vogler/free-games-claimer/issues/210#issuecomment-1727420943
|
||||||
if (checkoutUrl) {
|
console.error('Seems like currently there are no free games available in your region...');
|
||||||
console.log('Generated checkout URL:', checkoutUrl);
|
// urls below should then be an empty list
|
||||||
|
|
||||||
// Send notification with checkout link
|
|
||||||
await notify(`epic-games (${user}):<br>Free games available!<br>Click here to claim: <a href="${checkoutUrl}">${checkoutUrl}</a>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also save to database for reference
|
|
||||||
freeGames.forEach(game => {
|
|
||||||
const purchaseUrl = `https://store.epicgames.com/${game.productSlug}`;
|
|
||||||
db.data[user][game.offerId] ||= {
|
|
||||||
title: game.productName,
|
|
||||||
time: datetime(),
|
|
||||||
url: purchaseUrl,
|
|
||||||
checkoutUrl: checkoutUrl || purchaseUrl
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
// clicking on `game_sel` sometimes led to a 404, see https://github.com/vogler/free-games-claimer/issues/25
|
||||||
|
// 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
|
||||||
|
// 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 urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
|
||||||
|
console.log('Free games:', urls);
|
||||||
|
|
||||||
// Claim each game individually (for detailed tracking)
|
for (const url of urls) {
|
||||||
for (const game of freeGames) {
|
|
||||||
if (cfg.time) console.time('claim game');
|
if (cfg.time) console.time('claim game');
|
||||||
|
await page.goto(url); // , { waitUntil: 'domcontentloaded' });
|
||||||
const purchaseUrl = `https://store.epicgames.com/${game.productSlug}`;
|
const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"] >> :has-text("e"), :has-text("i")').first(); // when loading, the button text is empty -> need to wait for some text {'get', 'in library', 'requires base game'} -> just wait for e or i to not be too specific; :text-matches("\w+") somehow didn't work - https://github.com/vogler/free-games-claimer/issues/375
|
||||||
await page.goto(purchaseUrl);
|
|
||||||
|
|
||||||
const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"]').first();
|
|
||||||
await purchaseBtn.waitFor();
|
await purchaseBtn.waitFor();
|
||||||
const btnText = (await purchaseBtn.innerText()).toLowerCase();
|
const btnText = (await purchaseBtn.innerText()).toLowerCase(); // barrier to block until page is loaded
|
||||||
|
|
||||||
// click Continue if 'This game contains mature content recommended only for ages 18+'
|
// click Continue if 'This game contains mature content recommended only for ages 18+'
|
||||||
if (await page.locator('button:has-text("Continue")').count() > 0) {
|
if (await page.locator('button:has-text("Continue")').count() > 0) {
|
||||||
console.log(' This game contains mature content recommended only for ages 18+');
|
console.log(' This game contains mature content recommended only for ages 18+');
|
||||||
if (await page.locator('[data-testid="AgeSelect"]').count()) {
|
if (await page.locator('[data-testid="AgeSelect"]').count()) {
|
||||||
console.error(' Got "To continue, please provide your date of birth"');
|
console.error(' Got "To continue, please provide your date of birth" - This shouldn\'t happen due to cookie set above. Please report to https://github.com/vogler/free-games-claimer/issues/275');
|
||||||
await page.locator('#month_toggle').click();
|
await page.locator('#month_toggle').click();
|
||||||
await page.locator('#month_menu li:has-text("01")').click();
|
await page.locator('#month_menu li:has-text("01")').click();
|
||||||
await page.locator('#day_toggle').click();
|
await page.locator('#day_toggle').click();
|
||||||
|
|
@ -426,49 +199,66 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
let bundle_includes;
|
||||||
if (await page.locator('span:text-is("About Bundle")').count()) {
|
if (await page.locator('span:text-is("About Bundle")').count()) {
|
||||||
title = (await page.locator('span:has-text("Buy"):left-of([data-testid="purchase-cta-button"])').first().innerText()).replace('Buy ', '');
|
title = (await page.locator('span:has-text("Buy"):left-of([data-testid="purchase-cta-button"])').first().innerText()).replace('Buy ', '');
|
||||||
|
// h1 first didn't exist for bundles but now it does... However h1 would e.g. be 'Fallout® Classic Collection' instead of 'Fallout Classic Collection'
|
||||||
|
try {
|
||||||
|
bundle_includes = await Promise.all((await page.locator('.product-card-top-row h5').all()).map(b => b.innerText()));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get "Bundle Includes":', e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
title = await page.locator('h1').first().innerText();
|
title = await page.locator('h1').first().innerText();
|
||||||
}
|
}
|
||||||
|
const game_id = page.url().split('/').pop();
|
||||||
const existedInDb = db.data[user][game.offerId];
|
const existedInDb = db.data[user][game_id];
|
||||||
db.data[user][game.offerId] ||= { title, time: datetime(), url: purchaseUrl, checkoutUrl: checkoutUrl };
|
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:', chalk.blue(title));
|
console.log('Current free game:', chalk.blue(title));
|
||||||
|
if (bundle_includes) console.log(' This bundle includes:', bundle_includes);
|
||||||
|
const notify_game = { title, url, status: 'failed' };
|
||||||
|
notify_games.push(notify_game); // status is updated below
|
||||||
|
|
||||||
const notify_game = { title, url: purchaseUrl, status: 'failed' };
|
if (btnText == 'in library') {
|
||||||
notify_games.push(notify_game);
|
|
||||||
|
|
||||||
if (btnText == 'in library' || btnText == 'owned') {
|
|
||||||
console.log(' Already in library! Nothing to claim.');
|
console.log(' Already in library! Nothing to claim.');
|
||||||
if (!existedInDb) await notify(`Game already in library: ${purchaseUrl}`);
|
if (!existedInDb) await notify(`Game already in library: ${url}`);
|
||||||
notify_game.status = 'existed';
|
notify_game.status = 'existed';
|
||||||
db.data[user][game.offerId].status ||= 'existed';
|
db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed
|
||||||
if (db.data[user][game.offerId].status.startsWith('failed')) db.data[user][game.offerId].status = 'manual';
|
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 == 'requires base game') {
|
} else if (btnText == 'requires base game') {
|
||||||
console.log(' Requires base game! Nothing to claim.');
|
console.log(' Requires base game! Nothing to claim.');
|
||||||
notify_game.status = 'requires base game';
|
notify_game.status = 'requires base game';
|
||||||
db.data[user][game.offerId].status ||= 'failed:requires-base-game';
|
db.data[user][game_id].status ||= 'failed:requires-base-game';
|
||||||
} else {
|
// if base game is free, add to queue as well
|
||||||
|
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")');
|
||||||
|
// re-add original add-on to queue after base game
|
||||||
|
urls.push(baseUrl, url); // add base game to the list of games to claim and re-add add-on itself
|
||||||
|
} else { // GET
|
||||||
console.log(' Not in library yet! Click', btnText);
|
console.log(' Not in library yet! Click', btnText);
|
||||||
await purchaseBtn.click({ delay: 11 });
|
await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
|
||||||
|
|
||||||
// Accept EULA if shown
|
// Accept End User License Agreement (only needed once)
|
||||||
try {
|
const acceptEulaIfShown = async () => {
|
||||||
await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 });
|
try {
|
||||||
console.log(' Accept End User License Agreement');
|
await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 });
|
||||||
await page.locator('input#agree').check();
|
console.log(' Accept End User License Agreement (only needed once)');
|
||||||
await page.locator('button:has-text("Accept")').click();
|
await page.locator('input#agree').check();
|
||||||
} catch {
|
await page.locator('button:has-text("Accept")').click();
|
||||||
// EULA not shown
|
} catch {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
acceptEulaIfShown();
|
||||||
|
|
||||||
|
// it then creates an iframe for the purchase
|
||||||
await page.waitForSelector('#webPurchaseContainer iframe');
|
await page.waitForSelector('#webPurchaseContainer iframe');
|
||||||
const iframe = page.frameLocator('#webPurchaseContainer iframe');
|
const iframe = page.frameLocator('#webPurchaseContainer iframe');
|
||||||
|
// skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46
|
||||||
if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) {
|
if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) {
|
||||||
console.error(' This product is unavailable in your region!');
|
console.error(' This product is unavailable in your region!');
|
||||||
db.data[user][game.offerId].status = notify_game.status = 'unavailable-in-region';
|
db.data[user][game_id].status = notify_game.status = 'unavailable-in-region';
|
||||||
if (cfg.time) console.timeEnd('claim game');
|
if (cfg.time) console.timeEnd('claim game');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -496,77 +286,75 @@ try {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Playwright clicked before button was ready to handle event, https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1474346591
|
||||||
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
|
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 Accept")');
|
const btnAgree = iframe.locator('button:has-text("I Accept")');
|
||||||
try {
|
const acceptIfRequired = async () => {
|
||||||
await btnAgree.waitFor({ timeout: 10000 });
|
try {
|
||||||
await btnAgree.click();
|
await btnAgree.waitFor({ timeout: 10000 });
|
||||||
} catch {
|
await btnAgree.click();
|
||||||
// EU: wait for and click 'I Agree'
|
} catch {
|
||||||
}
|
return;
|
||||||
|
|
||||||
try {
|
|
||||||
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' });
|
|
||||||
db.data[user][game.offerId].status = 'claimed';
|
|
||||||
db.data[user][game.offerId].time = datetime();
|
|
||||||
console.log(' Claimed successfully!');
|
|
||||||
|
|
||||||
// Save device auth if we got a new token
|
|
||||||
const cookies = await context.cookies();
|
|
||||||
const bearerCookie = cookies.find(c => c.name === 'EPIC_BEARER_TOKEN');
|
|
||||||
if (bearerCookie?.value) {
|
|
||||||
await saveDeviceAuth(bearerCookie.value, 'refresh_token_placeholder', new Date(Date.now() + 86400000).toISOString());
|
|
||||||
}
|
}
|
||||||
|
}; // EU: wait for and click 'I Agree'
|
||||||
|
acceptIfRequired();
|
||||||
|
try {
|
||||||
|
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
|
||||||
|
const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe');
|
||||||
|
const watchCaptchaChallenge = async () => {
|
||||||
|
try {
|
||||||
|
await captcha.waitFor({ timeout: 10000 });
|
||||||
|
console.error(' Got hcaptcha challenge! Lost trust due to too many login attempts? You can solve the captcha in the browser or get a new IP address.');
|
||||||
|
await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}; // may time out if not shown
|
||||||
|
const watchCaptchaFailure = async () => {
|
||||||
|
try {
|
||||||
|
await iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor({ timeout: 10000 });
|
||||||
|
console.error(' Failed to challenge captcha, please try again later.');
|
||||||
|
await notify('epic-games: failed to challenge captcha. Please check.');
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watchCaptchaChallenge();
|
||||||
|
watchCaptchaFailure();
|
||||||
|
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' });
|
||||||
|
db.data[user][game_id].status = 'claimed';
|
||||||
|
db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time
|
||||||
|
console.log(' Claimed successfully!');
|
||||||
|
// context.setDefaultTimeout(cfg.timeout);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
// console.error(' Failed to claim! Try again if NopeCHA timed out. Click the extension to see if you ran out of credits (refill after 24h). To avoid captchas try to get a new IP or set a cookie from https://www.hcaptcha.com/accessibility');
|
||||||
console.error(' Failed to claim! To avoid captchas try to get a new IP address.');
|
console.error(' Failed to claim! To avoid captchas try to get a new IP address.');
|
||||||
const p = screenshot('failed', `${game.offerId}_${filenamify(datetime())}.png`);
|
const p = screenshot('failed', `${game_id}_${filenamify(datetime())}.png`);
|
||||||
await page.screenshot({ path: p, fullPage: true });
|
await page.screenshot({ path: p, fullPage: true });
|
||||||
db.data[user][game.offerId].status = 'failed';
|
db.data[user][game_id].status = 'failed';
|
||||||
}
|
}
|
||||||
notify_game.status = db.data[user][game.offerId].status;
|
notify_game.status = db.data[user][game_id].status; // claimed or failed
|
||||||
|
|
||||||
const p = screenshot(`${game.offerId}.png`);
|
const p = screenshot(`${game_id}.png`);
|
||||||
if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false });
|
if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long...
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.time) console.timeEnd('claim game');
|
if (cfg.time) console.timeEnd('claim game');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.time) console.timeEnd('claim all games');
|
if (cfg.time) console.timeEnd('claim all games');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.exitCode ||= 1;
|
process.exitCode ||= 1;
|
||||||
console.error('--- Exception:');
|
console.error('--- Exception:');
|
||||||
console.error(error);
|
console.error(error); // .toString()?
|
||||||
if (error.message && process.exitCode != 130) notify(`epic-games failed: ${error.message.split('\n')[0]}`);
|
if (error.message && process.exitCode != 130) notify(`epic-games failed: ${error.message.split('\n')[0]}`);
|
||||||
} finally {
|
} finally {
|
||||||
await db.write();
|
await db.write(); // write out json db
|
||||||
|
if (notify_games.filter(g => g.status == 'claimed' || g.status == 'failed').length) { // don't notify if all have status 'existed', 'manual', 'requires base game', 'unavailable-in-region', 'skipped'
|
||||||
// Save cookies
|
|
||||||
const cookies = await context.cookies();
|
|
||||||
// Convert cookies to EpicCookie format for setPuppeteerCookies
|
|
||||||
const epicCookies = cookies.map(c => ({
|
|
||||||
domain: c.domain,
|
|
||||||
hostOnly: !c.domain.startsWith('.'),
|
|
||||||
httpOnly: c.httpOnly,
|
|
||||||
name: c.name,
|
|
||||||
path: c.path,
|
|
||||||
sameSite: c.sameSite === 'Lax' ? 'no_restriction' : 'unspecified',
|
|
||||||
secure: c.secure,
|
|
||||||
session: !c.expires,
|
|
||||||
storeId: '0',
|
|
||||||
value: c.value,
|
|
||||||
id: 0,
|
|
||||||
expirationDate: c.expires ? Math.floor(c.expires) : undefined,
|
|
||||||
}));
|
|
||||||
await setPuppeteerCookies(cfg.eg_email || 'default', epicCookies);
|
|
||||||
|
|
||||||
if (notify_games.filter(g => g.status == 'claimed' || g.status == 'failed').length) {
|
|
||||||
notify(`epic-games (${user}):<br>${html_game_list(notify_games)}`);
|
notify(`epic-games (${user}):<br>${html_game_list(notify_games)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies()));
|
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());
|
if (page.video()) console.log('Recorded video:', await page.video().path());
|
||||||
await context.close();
|
await context.close();
|
||||||
|
|
|
||||||
17
gog.js
17
gog.js
|
|
@ -42,10 +42,7 @@ try {
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||||
|
|
||||||
const signIn = page.locator('a:has-text("Sign in")').first();
|
const signIn = page.locator('a:has-text("Sign in")').first();
|
||||||
await Promise.any([
|
await Promise.any([signIn.waitFor(), page.waitForSelector('#menuUsername')]);
|
||||||
signIn.waitFor({ timeout: cfg.login_visible_timeout }),
|
|
||||||
page.waitForSelector('#menuUsername', { timeout: cfg.login_visible_timeout }),
|
|
||||||
]).catch(() => {});
|
|
||||||
while (await signIn.isVisible()) {
|
while (await signIn.isVisible()) {
|
||||||
console.error('Not signed in anymore.');
|
console.error('Not signed in anymore.');
|
||||||
await signIn.click();
|
await signIn.click();
|
||||||
|
|
@ -59,11 +56,7 @@ 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) {
|
||||||
try {
|
iframe.locator('a[href="/logout"]').click().catch(_ => { }); // Click 'Change account' (email from previous login is set in some cookie)
|
||||||
await iframe.locator('a[href="/logout"]').click(); // Click 'Change account' (email from previous login is set in some cookie)
|
|
||||||
} catch {
|
|
||||||
// link not present, continue with login flow
|
|
||||||
}
|
|
||||||
await iframe.locator('#login_username').fill(email);
|
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();
|
||||||
|
|
@ -110,7 +103,9 @@ try {
|
||||||
|
|
||||||
const banner = page.locator('#giveaway');
|
const banner = page.locator('#giveaway');
|
||||||
const hasGiveaway = await banner.count();
|
const hasGiveaway = await banner.count();
|
||||||
if (hasGiveaway) {
|
if (!hasGiveaway) {
|
||||||
|
console.log('Currently no free giveaway!');
|
||||||
|
} else {
|
||||||
const text = await page.locator('.giveaway__content-header').innerText();
|
const text = await page.locator('.giveaway__content-header').innerText();
|
||||||
const match_all = text.match(/Claim (.*) and don't miss the|Success! (.*) was added to/);
|
const match_all = text.match(/Claim (.*) and don't miss the|Success! (.*) was added to/);
|
||||||
const title = match_all[1] ? match_all[1] : match_all[2];
|
const title = match_all[1] ? match_all[1] : match_all[2];
|
||||||
|
|
@ -151,8 +146,6 @@ try {
|
||||||
await page.locator('li:has-text("Marketing communications through Trusted Partners") label').uncheck();
|
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();
|
await page.locator('li:has-text("Promotions and hot deals") label').uncheck();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('Currently no free giveaway!');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.exitCode ||= 1;
|
process.exitCode ||= 1;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
sleep_for=${KEEP_ALIVE_SECONDS:-86400}
|
|
||||||
echo "Keeping container alive (interval ${sleep_for}s). Press Ctrl+C to stop."
|
|
||||||
|
|
||||||
trap 'exit 0' TERM INT
|
|
||||||
while true; do
|
|
||||||
sleep "$sleep_for" &
|
|
||||||
wait $!
|
|
||||||
done
|
|
||||||
275
package-lock.json
generated
275
package-lock.json
generated
|
|
@ -9,7 +9,6 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|
@ -22,8 +21,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin-js": "^4.2.0",
|
"@stylistic/eslint-plugin-js": "^4.2.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0"
|
||||||
"typescript": "^5.9.3"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=17"
|
"node": ">=17"
|
||||||
|
|
@ -448,23 +446,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"follow-redirects": "^1.15.6",
|
|
||||||
"form-data": "^4.0.4",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -546,6 +527,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -646,18 +628,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -782,15 +752,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
|
@ -832,6 +793,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
|
@ -881,6 +843,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -890,6 +853,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -899,6 +863,7 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
|
|
@ -907,21 +872,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-set-tostringtag": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"get-intrinsic": "^1.2.6",
|
|
||||||
"has-tostringtag": "^1.0.2",
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
|
@ -1375,26 +1325,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
|
||||||
"version": "1.15.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"debug": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/for-in": {
|
"node_modules/for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
|
|
@ -1416,43 +1346,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"es-set-tostringtag": "^2.1.0",
|
|
||||||
"hasown": "^2.0.2",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/form-data/node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/form-data/node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
|
@ -1497,6 +1390,7 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -1516,6 +1410,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
|
@ -1540,6 +1435,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
|
|
@ -1599,6 +1495,7 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -1626,6 +1523,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -1634,25 +1532,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-symbols": "^1.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -1983,6 +1867,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -2356,12 +2241,6 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -2877,20 +2756,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|
@ -3315,21 +3180,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
|
||||||
"integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q=="
|
"integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q=="
|
||||||
},
|
},
|
||||||
"asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
|
||||||
},
|
|
||||||
"axios": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
|
||||||
"requires": {
|
|
||||||
"follow-redirects": "^1.15.6",
|
|
||||||
"form-data": "^4.0.4",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -3382,6 +3232,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -3439,14 +3290,6 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
||||||
"requires": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -3526,11 +3369,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
||||||
},
|
},
|
||||||
"delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
|
||||||
},
|
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
|
@ -3554,6 +3392,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -3589,32 +3428,24 @@
|
||||||
"es-define-property": {
|
"es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"es-errors": {
|
"es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"es-object-atoms": {
|
"es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es-set-tostringtag": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
|
||||||
"requires": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"get-intrinsic": "^1.2.6",
|
|
||||||
"has-tostringtag": "^1.0.2",
|
|
||||||
"hasown": "^2.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"escalade": {
|
"escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
|
@ -3915,11 +3746,6 @@
|
||||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
|
||||||
"version": "1.15.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
|
|
||||||
},
|
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||||
|
|
@ -3933,33 +3759,6 @@
|
||||||
"for-in": "^1.0.1"
|
"for-in": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"form-data": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
|
||||||
"requires": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"es-set-tostringtag": "^2.1.0",
|
|
||||||
"hasown": "^2.0.2",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
|
||||||
},
|
|
||||||
"mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"requires": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
|
@ -3990,7 +3789,8 @@
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"generative-bayesian-network": {
|
"generative-bayesian-network": {
|
||||||
"version": "2.1.66",
|
"version": "2.1.66",
|
||||||
|
|
@ -4005,6 +3805,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
|
|
@ -4022,6 +3823,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
"es-object-atoms": "^1.0.0"
|
"es-object-atoms": "^1.0.0"
|
||||||
|
|
@ -4058,7 +3860,8 @@
|
||||||
"gopd": {
|
"gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
|
|
@ -4074,20 +3877,14 @@
|
||||||
"has-symbols": {
|
"has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
},
|
"dev": true
|
||||||
"has-tostringtag": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
||||||
"requires": {
|
|
||||||
"has-symbols": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"hasown": {
|
"hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
|
|
@ -4320,7 +4117,8 @@
|
||||||
"math-intrinsics": {
|
"math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"media-typer": {
|
"media-typer": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|
@ -4565,11 +4363,6 @@
|
||||||
"ipaddr.js": "1.9.1"
|
"ipaddr.js": "1.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proxy-from-env": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
|
||||||
},
|
|
||||||
"punycode": {
|
"punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -4878,12 +4671,6 @@
|
||||||
"mime-types": "^3.0.0"
|
"mime-types": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"universalify": {
|
"universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"node": ">=17"
|
"node": ">=17"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|
@ -33,7 +32,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin-js": "^4.2.0",
|
"@stylistic/eslint-plugin-js": "^4.2.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0"
|
||||||
"typescript": "^5.9.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,26 +45,6 @@ const handleMFA = async p => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForSignedInOrMFA = async p => {
|
|
||||||
const otpLocator = p.locator('#auth-mfa-otpcode, input[name=otpCode]');
|
|
||||||
const waitSignedIn = p.waitForURL('**/claims/**signedIn=true', { timeout: cfg.login_timeout }).then(() => true).catch(() => false);
|
|
||||||
const waitMFA = (async () => {
|
|
||||||
try {
|
|
||||||
await otpLocator.waitFor({ timeout: cfg.login_timeout });
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await handleMFA(p);
|
|
||||||
try {
|
|
||||||
await p.waitForURL('**/claims/**signedIn=true', { timeout: cfg.login_timeout });
|
|
||||||
} catch {
|
|
||||||
// if it still fails, caller will handle via timeout
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})();
|
|
||||||
await Promise.race([waitSignedIn, waitMFA]);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
|
||||||
const handleDirectLoginPage = async () => {
|
const handleDirectLoginPage = async () => {
|
||||||
|
|
@ -78,7 +58,7 @@ try {
|
||||||
await page.click('input[type="submit"]');
|
await page.click('input[type="submit"]');
|
||||||
await page.fill('[name=password]', password);
|
await page.fill('[name=password]', password);
|
||||||
await page.click('input[type="submit"]');
|
await page.click('input[type="submit"]');
|
||||||
await waitForSignedInOrMFA(page);
|
await handleMFA(page).catch(() => {});
|
||||||
try {
|
try {
|
||||||
await page.waitForURL('**/ap/signin**');
|
await page.waitForURL('**/ap/signin**');
|
||||||
const error = await page.locator('.a-alert-content').first().innerText();
|
const error = await page.locator('.a-alert-content').first().innerText();
|
||||||
|
|
@ -111,7 +91,7 @@ try {
|
||||||
'button:has-text("Anmelden")',
|
'button:has-text("Anmelden")',
|
||||||
'[data-a-target="user-dropdown-first-name-text"]',
|
'[data-a-target="user-dropdown-first-name-text"]',
|
||||||
'[data-testid="user-dropdown-first-name-text"]',
|
'[data-testid="user-dropdown-first-name-text"]',
|
||||||
].map(s => page.waitForSelector(s, { timeout: cfg.login_visible_timeout }))).catch(() => {});
|
].map(s => page.waitForSelector(s)));
|
||||||
try {
|
try {
|
||||||
await page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space when non-headless; could be flaky
|
await page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space when non-headless; could be flaky
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -131,7 +111,6 @@ try {
|
||||||
await page.click('input[type="submit"]');
|
await page.click('input[type="submit"]');
|
||||||
await page.fill('[name=password]', password);
|
await page.fill('[name=password]', password);
|
||||||
await page.click('input[type="submit"]');
|
await page.click('input[type="submit"]');
|
||||||
await waitForSignedInOrMFA(page);
|
|
||||||
try {
|
try {
|
||||||
await page.waitForURL('**/ap/signin**');
|
await page.waitForURL('**/ap/signin**');
|
||||||
const error = await page.locator('.a-alert-content').first().innerText();
|
const error = await page.locator('.a-alert-content').first().innerText();
|
||||||
|
|
@ -144,6 +123,11 @@ try {
|
||||||
} catch {
|
} catch {
|
||||||
// navigation ok
|
// navigation ok
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await handleMFA(page);
|
||||||
|
} catch {
|
||||||
|
// ignore MFA watcher errors
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Waiting for you to login in the browser.');
|
console.log('Waiting for you to login in the browser.');
|
||||||
await notify('prime-gaming: no longer signed in and not enough options set for automatic login.');
|
await notify('prime-gaming: no longer signed in and not enough options set for automatic login.');
|
||||||
|
|
@ -449,13 +433,13 @@ try {
|
||||||
}
|
}
|
||||||
// Disabled CTA (e.g., needs linking or not available)
|
// Disabled CTA (e.g., needs linking or not available)
|
||||||
if (await disabledCTA.count()) {
|
if (await disabledCTA.count()) {
|
||||||
if (store === 'epic-games') {
|
if (store !== 'epic-games') {
|
||||||
console.log(' CTA disabled for epic-games, will still try to link/claim.');
|
|
||||||
} else {
|
|
||||||
console.log(' CTA is disabled, skipping (likely needs linking/not available).');
|
console.log(' CTA is disabled, skipping (likely needs linking/not available).');
|
||||||
notify_game.status = 'disabled';
|
notify_game.status = 'disabled';
|
||||||
db.data[user][title] ||= { title, time: datetime(), url, store, status: 'disabled' };
|
db.data[user][title] ||= { title, time: datetime(), url, store, status: 'disabled' };
|
||||||
continue;
|
continue;
|
||||||
|
} else {
|
||||||
|
console.log(' CTA disabled for epic-games, will still try to link/claim.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (store == 'luna') {
|
if (store == 'luna') {
|
||||||
|
|
@ -680,19 +664,13 @@ try {
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
// most games have a button 'Get in-game content'
|
// most games have a button 'Get in-game content'
|
||||||
// epic-games: Fall Guys: Claim -> Continue -> Go to Epic Games (despite account linked and logged into epic-games) -> not tied to account but via some cookie?
|
// epic-games: Fall Guys: Claim -> Continue -> Go to Epic Games (despite account linked and logged into epic-games) -> not tied to account but via some cookie?
|
||||||
const claimAndContinue = async () => {
|
|
||||||
await page.click('.tw-button:has-text("Claim")');
|
|
||||||
try {
|
|
||||||
await page.click('button:has-text("Continue")');
|
|
||||||
} catch {
|
|
||||||
// continue button not always present
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const claimOptions = [
|
const claimOptions = [
|
||||||
page.click('.tw-button:has-text("Get in-game content")'),
|
page.click('.tw-button:has-text("Get in-game content")'),
|
||||||
page.click('.tw-button:has-text("Claim your gift")'),
|
page.click('.tw-button:has-text("Claim your gift")'),
|
||||||
claimAndContinue(),
|
(async () => {
|
||||||
|
await page.click('.tw-button:has-text("Claim")');
|
||||||
|
await page.click('button:has-text("Continue")').catch(() => {});
|
||||||
|
})(),
|
||||||
];
|
];
|
||||||
await Promise.any(claimOptions);
|
await Promise.any(claimOptions);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,3 @@ sonar.sources=.
|
||||||
|
|
||||||
#Eslint issues
|
#Eslint issues
|
||||||
sonar.eslint.reportPaths = eslint_report.json
|
sonar.eslint.reportPaths = eslint_report.json
|
||||||
|
|
||||||
# Ignore coverage and duplication requirements (community scan without reports)
|
|
||||||
sonar.coverage.exclusions=**/*
|
|
||||||
sonar.cpd.exclusions=**/*
|
|
||||||
# Ignore "commented-out code" findings (javascript:S125) across the project
|
|
||||||
sonar.issue.ignore.multicriteria=e1
|
|
||||||
sonar.issue.ignore.multicriteria.e1.ruleKey=javascript:S125
|
|
||||||
sonar.issue.ignore.multicriteria.e1.resourceKey=**/*
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,10 @@ export const cfg = {
|
||||||
get headless() {
|
get headless() {
|
||||||
return !this.debug && !this.show;
|
return !this.debug && !this.show;
|
||||||
},
|
},
|
||||||
eg_mode: process.env.EG_MODE || 'legacy', // epic-games: legacy playwright flow or 'new' API-driven flow
|
|
||||||
width: Number(process.env.WIDTH) || 1920, // width of the opened browser
|
width: Number(process.env.WIDTH) || 1920, // width of the opened browser
|
||||||
height: Number(process.env.HEIGHT) || 1080, // height of the opened browser
|
height: Number(process.env.HEIGHT) || 1080, // height of the opened browser
|
||||||
timeout: (Number(process.env.TIMEOUT) || 60) * 1000, // default timeout for playwright is 30s
|
timeout: (Number(process.env.TIMEOUT) || 60) * 1000, // default timeout for playwright is 30s
|
||||||
login_timeout: (Number(process.env.LOGIN_TIMEOUT) || 180) * 1000, // higher timeout for login, will wait twice: prompt + wait for manual login
|
login_timeout: (Number(process.env.LOGIN_TIMEOUT) || 180) * 1000, // higher timeout for login, will wait twice: prompt + wait for manual login
|
||||||
login_visible_timeout: (Number(process.env.LOGIN_VISIBLE_TIMEOUT) || 20) * 1000, // how long to wait for login button/user indicator to appear
|
|
||||||
novnc_port: process.env.NOVNC_PORT, // running in docker if set
|
novnc_port: process.env.NOVNC_PORT, // running in docker if set
|
||||||
notify: process.env.NOTIFY, // apprise notification services
|
notify: process.env.NOTIFY, // apprise notification services
|
||||||
notify_title: process.env.NOTIFY_TITLE, // apprise notification title
|
notify_title: process.env.NOTIFY_TITLE, // apprise notification title
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// Epic Games API Constants
|
|
||||||
// Based on https://github.com/claabs/epicgames-freegames-node
|
|
||||||
|
|
||||||
export const EPIC_CLIENT_ID = '875a3b57d3a640a6b7f9b4e883463ab4';
|
|
||||||
export const CSRF_ENDPOINT = 'https://www.epicgames.com/id/api/csrf';
|
|
||||||
export const ACCOUNT_CSRF_ENDPOINT = 'https://www.epicgames.com/account/v2/refresh-csrf';
|
|
||||||
export const ACCOUNT_SESSION_ENDPOINT = 'https://www.epicgames.com/account/personal';
|
|
||||||
export const LOGIN_ENDPOINT = 'https://www.epicgames.com/id/api/login';
|
|
||||||
export const REDIRECT_ENDPOINT = 'https://www.epicgames.com/id/api/redirect';
|
|
||||||
export const GRAPHQL_ENDPOINT = 'https://store.epicgames.com/graphql';
|
|
||||||
export const ARKOSE_BASE_URL = 'https://epic-games-api.arkoselabs.com';
|
|
||||||
export const CHANGE_EMAIL_ENDPOINT = 'https://www.epicgames.com/account/v2/api/email/change';
|
|
||||||
export const USER_INFO_ENDPOINT = 'https://www.epicgames.com/account/v2/personal/ajaxGet';
|
|
||||||
export const RESEND_VERIFICATION_ENDPOINT = 'https://www.epicgames.com/account/v2/resendEmailVerification';
|
|
||||||
export const REPUTATION_ENDPOINT = 'https://www.epicgames.com/id/api/reputation';
|
|
||||||
export const STORE_CONTENT = 'https://store-content-ipv4.ak.epicgames.com/api/en-US/content';
|
|
||||||
export const EMAIL_VERIFY = 'https://www.epicgames.com/id/api/email/verify';
|
|
||||||
export const SETUP_MFA = 'https://www.epicgames.com/account/v2/security/ajaxUpdateTwoFactorAuthSettings';
|
|
||||||
export const FREE_GAMES_PROMOTIONS_ENDPOINT = 'https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions';
|
|
||||||
export const STORE_HOMEPAGE = 'https://store.epicgames.com/';
|
|
||||||
export const STORE_HOMEPAGE_EN = `${STORE_HOMEPAGE}en-US/`;
|
|
||||||
export const STORE_CART_EN = `${STORE_HOMEPAGE}en-US/cart`;
|
|
||||||
export const ORDER_CONFIRM_ENDPOINT = 'https://payment-website-pci.ol.epicgames.com/purchase/confirm-order';
|
|
||||||
export const ORDER_PREVIEW_ENDPOINT = 'https://payment-website-pci.ol.epicgames.com/purchase/order-preview';
|
|
||||||
export const EPIC_PURCHASE_ENDPOINT = 'https://www.epicgames.com/store/purchase';
|
|
||||||
export const MFA_LOGIN_ENDPOINT = 'https://www.epicgames.com/id/api/login/mfa';
|
|
||||||
export const UNREAL_SET_SID_ENDPOINT = 'https://www.unrealengine.com/id/api/set-sid';
|
|
||||||
export const TWINMOTION_SET_SID_ENDPOINT = 'https://www.twinmotion.com/id/api/set-sid';
|
|
||||||
export const CLIENT_REDIRECT_ENDPOINT = `https://www.epicgames.com/id/api/client/${EPIC_CLIENT_ID}`;
|
|
||||||
export const AUTHENTICATE_ENDPOINT = `https://www.epicgames.com/id/api/authenticate`;
|
|
||||||
export const LOCATION_ENDPOINT = `https://www.epicgames.com/id/api/location`;
|
|
||||||
export const PHASER_F_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/phaser/f';
|
|
||||||
export const PHASER_BATCH_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/phaser/batch';
|
|
||||||
export const TALON_IP_ENDPOINT = 'https://talon-service-v4-prod.ak.epicgames.com/v1/init/ip';
|
|
||||||
export const TALON_INIT_ENDPOINT = 'https://talon-service-prod.ak.epicgames.com/v1/init';
|
|
||||||
export const TALON_EXECUTE_ENDPOINT = 'https://talon-service-v4-prod.ak.epicgames.com/v1/init/execute';
|
|
||||||
export const TALON_WEBSITE_BASE = 'https://talon-website-prod.ak.epicgames.com';
|
|
||||||
export const TALON_REFERRER = 'https://talon-website-prod.ak.epicgames.com/challenge?env=prod&flow=login_prod&origin=https%3A%2F%2Fwww.epicgames.com';
|
|
||||||
export const ACCOUNT_OAUTH_TOKEN = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token';
|
|
||||||
export const ACCOUNT_OAUTH_DEVICE_AUTH = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/deviceAuthorization';
|
|
||||||
export const ID_LOGIN_ENDPOINT = 'https://www.epicgames.com/id/login';
|
|
||||||
export const EULA_AGREEMENTS_ENDPOINT = 'https://eulatracking-public-service-prod-m.ol.epicgames.com/eulatracking/api/public/agreements';
|
|
||||||
export const REQUIRED_EULAS = ['epicgames_privacy_policy_no_table', 'egstore'];
|
|
||||||
171
src/cookie.ts
171
src/cookie.ts
|
|
@ -1,171 +0,0 @@
|
||||||
// Cookie management for Epic Games
|
|
||||||
// Based on https://github.com/claabs/epicgames-freegames-node
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import tough from 'tough-cookie';
|
|
||||||
import { filenamify } from './util.js';
|
|
||||||
import { dataDir } from './util.js';
|
|
||||||
|
|
||||||
const CONFIG_DIR = dataDir('config');
|
|
||||||
const DEFAULT_COOKIE_NAME = 'default';
|
|
||||||
|
|
||||||
// Ensure config directory exists
|
|
||||||
if (!fs.existsSync(CONFIG_DIR)) {
|
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookiePath(username) {
|
|
||||||
const fileSafeUsername = filenamify(username);
|
|
||||||
const cookieFilename = path.join(CONFIG_DIR, `${fileSafeUsername}-cookies.json`);
|
|
||||||
return cookieFilename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie whitelist - only these cookies are stored
|
|
||||||
const COOKIE_WHITELIST = ['EPIC_SSO_RM', 'EPIC_SESSION_AP', 'EPIC_DEVICE'];
|
|
||||||
|
|
||||||
// Cookie jar cache
|
|
||||||
const cookieJars = new Map();
|
|
||||||
|
|
||||||
function getCookieJar(username) {
|
|
||||||
let cookieJar = cookieJars.get(username);
|
|
||||||
if (cookieJar) {
|
|
||||||
return cookieJar;
|
|
||||||
}
|
|
||||||
const cookieFilename = getCookiePath(username);
|
|
||||||
cookieJar = new tough.CookieJar();
|
|
||||||
cookieJars.set(username, cookieJar);
|
|
||||||
return cookieJar;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert EditThisCookie format to tough-cookie file store format
|
|
||||||
export function editThisCookieToToughCookieFileStore(etc) {
|
|
||||||
const tcfs = {};
|
|
||||||
|
|
||||||
etc.forEach((etcCookie) => {
|
|
||||||
const domain = etcCookie.domain.replace(/^\./, '');
|
|
||||||
const expires = etcCookie.expirationDate
|
|
||||||
? new Date(etcCookie.expirationDate * 1000).toISOString()
|
|
||||||
: undefined;
|
|
||||||
const { path: cookiePath, name } = etcCookie;
|
|
||||||
|
|
||||||
if (COOKIE_WHITELIST.includes(name)) {
|
|
||||||
const temp = {
|
|
||||||
[domain]: {
|
|
||||||
[cookiePath]: {
|
|
||||||
[name]: {
|
|
||||||
key: name,
|
|
||||||
value: etcCookie.value,
|
|
||||||
expires,
|
|
||||||
domain,
|
|
||||||
path: cookiePath,
|
|
||||||
secure: etcCookie.secure,
|
|
||||||
httpOnly: etcCookie.httpOnly,
|
|
||||||
hostOnly: etcCookie.hostOnly,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Object.assign(tcfs, temp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tcfs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cookies as simple object
|
|
||||||
export function getCookies(username) {
|
|
||||||
const cookieJar = getCookieJar(username);
|
|
||||||
const cookies = cookieJar.toJSON()?.cookies || [];
|
|
||||||
return cookies.reduce((accum, cookie) => {
|
|
||||||
if (cookie.key && cookie.value) {
|
|
||||||
return { ...accum, [cookie.key]: cookie.value };
|
|
||||||
}
|
|
||||||
return accum;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get raw cookies in tough-cookie file store format
|
|
||||||
export async function getCookiesRaw(username) {
|
|
||||||
const cookieFilename = getCookiePath(username);
|
|
||||||
try {
|
|
||||||
const existingCookies = JSON.parse(fs.readFileSync(cookieFilename, 'utf8'));
|
|
||||||
return existingCookies;
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cookies from Playwright/Cookie format
|
|
||||||
export async function setPuppeteerCookies(username, newCookies) {
|
|
||||||
const cookieJar = getCookieJar(username);
|
|
||||||
|
|
||||||
for (const cookie of newCookies) {
|
|
||||||
const domain = cookie.domain.replace(/^\./, '');
|
|
||||||
const tcfsCookie = new tough.Cookie({
|
|
||||||
key: cookie.name,
|
|
||||||
value: cookie.value,
|
|
||||||
expires: cookie.expires ? new Date(cookie.expires * 1000) : undefined,
|
|
||||||
domain,
|
|
||||||
path: cookie.path,
|
|
||||||
secure: cookie.secure,
|
|
||||||
httpOnly: cookie.httpOnly,
|
|
||||||
hostOnly: !cookie.domain.startsWith('.'),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await cookieJar.setCookie(tcfsCookie, `https://${domain}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error setting cookie:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete cookies for a user
|
|
||||||
export async function deleteCookies(username) {
|
|
||||||
const cookieFilename = getCookiePath(username || DEFAULT_COOKIE_NAME);
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(cookieFilename);
|
|
||||||
} catch {
|
|
||||||
// File doesn't exist, that's fine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has a valid cookie
|
|
||||||
export async function userHasValidCookie(username, cookieName) {
|
|
||||||
const cookieFilename = getCookiePath(username);
|
|
||||||
try {
|
|
||||||
const fileExists = fs.existsSync(cookieFilename);
|
|
||||||
if (!fileExists) return false;
|
|
||||||
|
|
||||||
const cookieData = JSON.parse(fs.readFileSync(cookieFilename, 'utf8'));
|
|
||||||
const rememberCookieExpireDate = cookieData['epicgames.com']?.['/']?.[cookieName]?.expires;
|
|
||||||
if (!rememberCookieExpireDate) return false;
|
|
||||||
|
|
||||||
return new Date(rememberCookieExpireDate) > new Date();
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert imported cookies (EditThisCookie format)
|
|
||||||
export async function convertImportCookies(username) {
|
|
||||||
const cookieFilename = getCookiePath(username);
|
|
||||||
const fileExists = fs.existsSync(cookieFilename);
|
|
||||||
|
|
||||||
if (fileExists) {
|
|
||||||
try {
|
|
||||||
const cookieData = fs.readFileSync(cookieFilename, 'utf8');
|
|
||||||
const cookieTest = JSON.parse(cookieData);
|
|
||||||
|
|
||||||
if (Array.isArray(cookieTest)) {
|
|
||||||
// Convert from EditThisCookie format
|
|
||||||
const tcfsCookies = editThisCookieToToughCookieFileStore(cookieTest);
|
|
||||||
fs.writeFileSync(cookieFilename, JSON.stringify(tcfsCookies, null, 2));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Invalid format, delete file
|
|
||||||
fs.unlinkSync(cookieFilename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
// Device authentication management for Epic Games
|
|
||||||
// Based on https://github.com/claabs/epicgames-freegames-node
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { dataDir } from './util.js';
|
|
||||||
|
|
||||||
const CONFIG_DIR = dataDir('config');
|
|
||||||
const deviceAuthsFilename = path.join(CONFIG_DIR, 'device-auths.json');
|
|
||||||
|
|
||||||
// Ensure config directory exists
|
|
||||||
if (!fs.existsSync(CONFIG_DIR)) {
|
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDeviceAuths() {
|
|
||||||
try {
|
|
||||||
const deviceAuths = JSON.parse(fs.readFileSync(deviceAuthsFilename, 'utf-8'));
|
|
||||||
return deviceAuths;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccountAuth(account) {
|
|
||||||
const deviceAuths = await getDeviceAuths();
|
|
||||||
return deviceAuths?.[account];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeDeviceAuths(deviceAuths) {
|
|
||||||
fs.writeFileSync(deviceAuthsFilename, JSON.stringify(deviceAuths, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setAccountAuth(account, accountAuth) {
|
|
||||||
const existingDeviceAuths = (await getDeviceAuths()) ?? {};
|
|
||||||
existingDeviceAuths[account] = accountAuth;
|
|
||||||
await writeDeviceAuths(existingDeviceAuths);
|
|
||||||
}
|
|
||||||
|
|
@ -98,11 +98,7 @@ try {
|
||||||
console.log(`Signed in as ${user}`);
|
console.log(`Signed in as ${user}`);
|
||||||
db.data[user] ||= {};
|
db.data[user] ||= {};
|
||||||
|
|
||||||
try {
|
page.locator('button:has-text("Accept All Cookies")').click().catch(_ => { });
|
||||||
await page.locator('button:has-text("Accept All Cookies")').click();
|
|
||||||
} catch {
|
|
||||||
// button may not be present
|
|
||||||
}
|
|
||||||
|
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const p of await page.locator('article.asset').all()) {
|
for (const p of await page.locator('article.asset').all()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue