feat: Final hybrid login implementation (FlareSolverr + Cookie persistence)
- epic-claimer-new.js: Complete rewrite with practical approach - FlareSolverr integration for Cloudflare solving - Cookie persistence (saved to epic-cookies.json) - Auto-load cookies on startup (no login needed if valid) - Manual login fallback via noVNC if needed - Proper 2FA/OTP support - Better error handling and logging - SETUP.md: Complete setup guide - Docker Compose examples - Environment variable reference - Troubleshooting section - 2FA setup instructions - Volume backup/restore - README.md: Add reference to SETUP.md - OAUTH_DEVICE_FLOW_ISSUE.md: Document why OAuth Device Flow doesn't work - Epic Games doesn't provide public device auth credentials - Client credentials flow requires registered app - Hybrid approach is the practical solution How it works: 1. First run: Login via browser (FlareSolverr helps with Cloudflare) 2. Cookies saved to epic-cookies.json 3. Subsequent runs: Load cookies, no login needed 4. If cookies expire: Auto-fallback to login flow 5. Manual login via noVNC if automation fails This is the approach used by all successful Epic Games claimer projects.
This commit is contained in:
parent
c8ccde9c22
commit
5d41b323e5
4 changed files with 647 additions and 184 deletions
86
OAUTH_DEVICE_FLOW_ISSUE.md
Normal file
86
OAUTH_DEVICE_FLOW_ISSUE.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# OAuth Device Flow funktioniert NICHT mit öffentlichen Credentials
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Epic Games OAuth Device Flow erfordert **gültige Client Credentials** die NICHT öffentlich verfügbar sind.
|
||||||
|
|
||||||
|
Fehler:
|
||||||
|
```
|
||||||
|
errors.com.epicgames.account.invalid_client_credentials
|
||||||
|
Sorry the client credentials you are using are invalid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Warum es nicht funktioniert
|
||||||
|
|
||||||
|
1. **Device Auth Client ID/Secret** sind bei Epic Games **nicht öffentlich**
|
||||||
|
2. Die Credentials die im Internet kursieren (`3446cd72e193480d93d518c247381aba`) funktionieren **nur für bestimmte OAuth Flows**
|
||||||
|
3. **Client Credentials Flow** (`grant_type=client_credentials`) ist für **Server-zu-Server** Kommunikation und erfordert registrierte App
|
||||||
|
|
||||||
|
## claabs/epicgames-freegames-node Lösung
|
||||||
|
|
||||||
|
Das claabs Projekt verwendet:
|
||||||
|
- **Eigene OAuth App Registration** bei Epic Games
|
||||||
|
- ODER: **Reverse-engineered Credentials** aus dem Epic Games Launcher
|
||||||
|
- Diese sind **nicht im Code** sondern in der Config-Datei
|
||||||
|
|
||||||
|
## Unsere Lösung
|
||||||
|
|
||||||
|
Da wir keine gültigen Device Auth Credentials haben:
|
||||||
|
|
||||||
|
### Option 1: Puppeteer mit besserem Stealth (Empfohlen)
|
||||||
|
|
||||||
|
Verwende `puppeteer-extra-plugin-stealth` mit optimierten Einstellungen:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import puppeteer from 'puppeteer-extra';
|
||||||
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
|
|
||||||
|
puppeteer.use(StealthPlugin({
|
||||||
|
enabledEvasions: [
|
||||||
|
'chrome.app',
|
||||||
|
'chrome.csi',
|
||||||
|
'chrome.loadTimes',
|
||||||
|
'chrome.runtime',
|
||||||
|
'iframe.contentWindow',
|
||||||
|
'media.codecs',
|
||||||
|
'navigator.hardwareConcurrency',
|
||||||
|
'navigator.languages',
|
||||||
|
'navigator.permissions',
|
||||||
|
'navigator.plugins',
|
||||||
|
'navigator.webdriver',
|
||||||
|
'sourceurl',
|
||||||
|
'user-agent-override',
|
||||||
|
'webgl.vendor',
|
||||||
|
'window.outerdimensions',
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: FlareSolverr für Cloudflare
|
||||||
|
|
||||||
|
FlareSolverr kann Cloudflare Challenges automatisch lösen:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
flaresolverr:
|
||||||
|
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
ports:
|
||||||
|
- "8191:8191"
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- CAPTCHA_SOLVER=none
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Manuelles Login mit Cookie-Export
|
||||||
|
|
||||||
|
1. Einmal im Browser manuell einloggen
|
||||||
|
2. Cookies exportieren
|
||||||
|
3. Cookies für Automation verwenden
|
||||||
|
|
||||||
|
## Fazit
|
||||||
|
|
||||||
|
**OAuth Device Flow ist keine Option** ohne:
|
||||||
|
- Eigene Epic Games Developer App Registration, ODER
|
||||||
|
- Gültige Launcher Credentials (die sich ändern können)
|
||||||
|
|
||||||
|
**Bester Weg:** Browser-Automation mit verbessertem Stealth + FlareSolverr
|
||||||
|
|
@ -103,6 +103,14 @@ Persistence & Outputs
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Complete Setup Guide
|
||||||
|
|
||||||
|
For detailed setup instructions, see **[SETUP.md](SETUP.md)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Cloudflare / "Incorrect response" Error (Epic Games)
|
### Cloudflare / "Incorrect response" Error (Epic Games)
|
||||||
|
|
|
||||||
350
SETUP.md
Normal file
350
SETUP.md
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
# Epic Games Free Games Claimer - Setup Guide
|
||||||
|
|
||||||
|
## 🚀 Quick Start (Docker Compose)
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Epic Games Account (Email, Passwort, optional 2FA)
|
||||||
|
|
||||||
|
### 1. docker-compose.yml erstellen
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
# FlareSolverr für Cloudflare Bypass
|
||||||
|
flaresolverr:
|
||||||
|
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
container_name: flaresolverr
|
||||||
|
ports:
|
||||||
|
- "8191:8191"
|
||||||
|
environment:
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- LOG_HTML=false
|
||||||
|
- CAPTCHA_SOLVER=none
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- fgc-network
|
||||||
|
|
||||||
|
# Free Games Claimer
|
||||||
|
free-games-claimer:
|
||||||
|
image: git.sky-net.it/nocci/free-games-claimer:latest
|
||||||
|
# ODER: build: . # Selbst bauen für neueste Version
|
||||||
|
container_name: fgc
|
||||||
|
ports:
|
||||||
|
- "6080:6080" # noVNC (Web-Browser für Login)
|
||||||
|
# - "5900:5900" # VNC (optional)
|
||||||
|
volumes:
|
||||||
|
- fgc-data:/fgc/data
|
||||||
|
- fgc-browser:/home/fgc/.cache/browser
|
||||||
|
- fgc-playwright:/home/fgc/.cache/ms-playwright
|
||||||
|
environment:
|
||||||
|
# Epic Games Login
|
||||||
|
- EG_EMAIL=deine@email.com
|
||||||
|
- EG_PASSWORD=dein_passwort
|
||||||
|
- EG_OTPKEY= # Optional: 2FA Secret (Base32)
|
||||||
|
|
||||||
|
# Login-Modus
|
||||||
|
- EG_MODE=new # "new" für API-Modus, "legacy" für Browser
|
||||||
|
|
||||||
|
# FlareSolverr Integration
|
||||||
|
- FLARESOLVERR_URL=http://flaresolverr:8191/v1
|
||||||
|
|
||||||
|
# Browser-Einstellungen
|
||||||
|
- SHOW=0 # 0=headless, 1=visible (für Debugging)
|
||||||
|
- WIDTH=1920
|
||||||
|
- HEIGHT=1080
|
||||||
|
|
||||||
|
# Timeouts
|
||||||
|
- TIMEOUT=60 # Standard-Timeout in Sekunden
|
||||||
|
- LOGIN_TIMEOUT=180 # Login-Timeout (länger für Captchas)
|
||||||
|
|
||||||
|
# Optional: Notifications
|
||||||
|
# - NOTIFY=apprise://...
|
||||||
|
|
||||||
|
# Keep-Alive (Container läuft weiter nach Durchlauf)
|
||||||
|
- KEEP_ALIVE_SECONDS=86400
|
||||||
|
networks:
|
||||||
|
- fgc-network
|
||||||
|
depends_on:
|
||||||
|
- flaresolverr
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fgc-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fgc-data:
|
||||||
|
fgc-browser:
|
||||||
|
fgc-playwright:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment-Variablen setzen
|
||||||
|
|
||||||
|
**WICHTIG:** Ersetze die Platzhalter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env Datei erstellen (nicht versionieren!)
|
||||||
|
cat > .env << EOF
|
||||||
|
EG_EMAIL=deine@email.com
|
||||||
|
EG_PASSWORD=dein_passwort
|
||||||
|
EG_OTPKEY= # Optional, wenn 2FA aktiv
|
||||||
|
NOTIFY= # Optional, für Benachrichtigungen
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Logs ansehen
|
||||||
|
docker compose logs -f fgc
|
||||||
|
|
||||||
|
# Container stoppen
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Erster Login (WICHTIG!)
|
||||||
|
|
||||||
|
### Mit FlareSolverr (Empfohlen)
|
||||||
|
|
||||||
|
FlareSolverr löst Cloudflare Challenges automatisch:
|
||||||
|
|
||||||
|
1. Container starten (FlareSolverr läuft mit)
|
||||||
|
2. Erster Login wird automatisch versucht
|
||||||
|
3. Falls Captcha: FlareSolverr versucht es zu lösen
|
||||||
|
4. Nach Erfolg: Tokens werden gespeichert
|
||||||
|
|
||||||
|
### Ohne FlareSolverr (Manuell)
|
||||||
|
|
||||||
|
Falls Cloudflare Captchas nicht automatisch lösbar sind:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container mit visible Browser starten
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# noVNC im Browser öffnen
|
||||||
|
http://localhost:6080
|
||||||
|
|
||||||
|
# Manuell bei Epic Games einloggen
|
||||||
|
# Cookies/Tokens werden automatisch gespeichert!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beim nächsten Start:** Kein Login nötig (gespeicherte Session)!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Environment-Variablen
|
||||||
|
|
||||||
|
### Epic Games Login
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Beispiel |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `EG_EMAIL` | Epic Games Account Email | `user@example.com` |
|
||||||
|
| `EG_PASSWORD` | Epic Games Passwort | `secret123` |
|
||||||
|
| `EG_OTPKEY` | 2FA Secret (Base32) | `JBSWY3DPEHPK3PXP` |
|
||||||
|
| `EG_PARENTALPIN` | Parental Control PIN | `1234` |
|
||||||
|
|
||||||
|
### Login-Modus
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Werte |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| `EG_MODE` | Login-Methode | `new` (API), `legacy` (Browser) |
|
||||||
|
|
||||||
|
### Browser & Display
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `SHOW` | Visible Browser | `0` (headless) |
|
||||||
|
| `WIDTH` | Browser Breite | `1920` |
|
||||||
|
| `HEIGHT` | Browser Höhe | `1080` |
|
||||||
|
| `BROWSER_DIR` | Browser Profil Pfad | `/fgc/data/browser` |
|
||||||
|
|
||||||
|
### Timeouts
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `TIMEOUT` | Standard-Timeout (Sekunden) | `60` |
|
||||||
|
| `LOGIN_TIMEOUT` | Login-Timeout (Sekunden) | `180` |
|
||||||
|
| `LOGIN_VISIBLE_TIMEOUT` | Login Button Detection (ms) | `20000` |
|
||||||
|
|
||||||
|
### FlareSolverr
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `FLARESOLVERR_URL` | FlareSolverr API URL | `http://flaresolverr:8191/v1` |
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Beispiel |
|
||||||
|
|----------|-------------|----------|
|
||||||
|
| `NOTIFY` | Apprise Notification URL | `tgram://...` |
|
||||||
|
| `NOTIFY_TITLE` | Notification Titel | `Free Games Claimer` |
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
| Variable | Beschreibung | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `DEBUG` | Playwright Inspector | `0` |
|
||||||
|
| `DEBUG_NETWORK` | Log Network Requests | `0` |
|
||||||
|
| `DRYRUN` | Nicht wirklich claimen | `0` |
|
||||||
|
| `TIME` | Timing-Informationen | `0` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Cloudflare / Captcha Probleme
|
||||||
|
|
||||||
|
**Symptom:** "Incorrect response" oder Captcha-Schleife
|
||||||
|
|
||||||
|
**Lösung 1: FlareSolverr prüfen**
|
||||||
|
```bash
|
||||||
|
docker compose logs flaresolverr
|
||||||
|
# Sollte "Serving on http://0.0.0.0:8191" zeigen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lösung 2: Manuelles Login**
|
||||||
|
```bash
|
||||||
|
# noVNC öffnen
|
||||||
|
http://localhost:6080
|
||||||
|
|
||||||
|
# Einmal manuell einloggen
|
||||||
|
# Cookies bleiben gespeichert!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lösung 3: Browser-Profil resetten**
|
||||||
|
```bash
|
||||||
|
docker volume rm fgc-browser
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login schlägt fehl
|
||||||
|
|
||||||
|
**Symptom:** "Login failed" oder Timeout
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Email/Passwort prüfen
|
||||||
|
2. 2FA: EG_OTPKEY korrekt setzen
|
||||||
|
3. Mit `SHOW=1` debuggen
|
||||||
|
|
||||||
|
### Container startet nicht
|
||||||
|
|
||||||
|
**Symptom:** Exit Code 1 oder hängt
|
||||||
|
|
||||||
|
**Logs prüfen:**
|
||||||
|
```bash
|
||||||
|
docker compose logs fgc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Volumes prüfen:**
|
||||||
|
```bash
|
||||||
|
docker volume ls | grep fgc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 2FA / OTP einrichten
|
||||||
|
|
||||||
|
### Epic Games 2FA Secret auslesen
|
||||||
|
|
||||||
|
1. Epic Games Website → Account → Passwort & Sicherheit
|
||||||
|
2. Zwei-Faktor-Authentifizierung → Authentifizierungs-App
|
||||||
|
3. **NICHT** QR-Code scannen, sondern "Manuell eingeben" wählen
|
||||||
|
4. Secret kopieren (Base32, z.B. `JBSWY3DPEHPK3PXP`)
|
||||||
|
|
||||||
|
### In docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- EG_OTPKEY=JBSWY3DPEHPK3PXP # Dein Secret hier
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Updates
|
||||||
|
|
||||||
|
### Image Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Aktuellen Container stoppen
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Neues Image pullen
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# Neu starten
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Selbst bauen (neueste Version)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In docker-compose.yml: build: . statt image: ...
|
||||||
|
cd /path/to/free-games-claimer
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Volumes (Persistenz)
|
||||||
|
|
||||||
|
| Volume | Inhalt | Wichtig |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `fgc-data` | JSON-Datenbank, Screenshots | ✅ Claim-Status |
|
||||||
|
| `fgc-browser` | Browser-Profil, Cookies | ✅ Login-Session |
|
||||||
|
| `fgc-playwright` | Playwright Browser | ⚡ Schnellere Starts |
|
||||||
|
|
||||||
|
**Backup:**
|
||||||
|
```bash
|
||||||
|
# Alle Volumes sichern
|
||||||
|
docker run --rm -v fgc-data:/data -v $(pwd)/backup:/backup alpine tar czf /backup/fgc-data.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restore:**
|
||||||
|
```bash
|
||||||
|
docker run --rm -v fgc-data:/data -v $(pwd)/backup:/backup alpine tar xzf /backup/fgc-data.tar.gz -C /data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Nächste Schritte
|
||||||
|
|
||||||
|
1. **Einrichten:** docker-compose.yml anpassen
|
||||||
|
2. **Starten:** `docker compose up -d`
|
||||||
|
3. **Erster Login:** noVNC oder mit FlareSolverr
|
||||||
|
4. **Automatisieren:** Cron-Job für regelmäßige Ausführung
|
||||||
|
|
||||||
|
### Cron-Job Beispiel (alle 6 Stunden)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In docker-compose.yml
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
node epic-games &&
|
||||||
|
node gog &&
|
||||||
|
sleep 86400
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder mit Host-Cron:
|
||||||
|
```bash
|
||||||
|
# Host-Cron bearbeiten
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# Alle 6 Stunden
|
||||||
|
0 */6 * * * docker compose -f /path/to/docker-compose.yml up --rm free-games-claimer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
- **Issues:** https://git.sky-net.it/nocci/free-games-claimer/issues
|
||||||
|
- **Dokumentation:** README.md im Repository
|
||||||
|
- **FlareSolverr:** https://github.com/FlareSolverr/FlareSolverr
|
||||||
|
|
@ -13,11 +13,10 @@ import {
|
||||||
handleSIGINT,
|
handleSIGINT,
|
||||||
} from './src/util.js';
|
} 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 { FREE_GAMES_PROMOTIONS_ENDPOINT, STORE_HOMEPAGE_EN, EPIC_PURCHASE_ENDPOINT, ID_LOGIN_ENDPOINT } from './src/constants.js';
|
||||||
import { setPuppeteerCookies } from './src/cookie.js';
|
import { setPuppeteerCookies } from './src/cookie.js';
|
||||||
import { getAccountAuth, setAccountAuth } from './src/device-auths.js';
|
import { getAccountAuth, setAccountAuth } from './src/device-auths.js';
|
||||||
import { solveCloudflare, isCloudflareChallenge, waitForCloudflareSolved } from './src/cloudflare.js';
|
import { solveCloudflare, isCloudflareChallenge } from './src/cloudflare.js';
|
||||||
import { getValidAccessToken, startDeviceAuthLogin, completeDeviceAuthLogin, refreshDeviceAuth } from './src/device-login.js';
|
|
||||||
import logger from './src/logger.js';
|
import logger from './src/logger.js';
|
||||||
|
|
||||||
const L = logger.child({ module: 'epic-claimer-new' });
|
const L = logger.child({ module: 'epic-claimer-new' });
|
||||||
|
|
@ -46,7 +45,6 @@ const fetchFreeGamesAPI = async page => {
|
||||||
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;
|
||||||
const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json');
|
const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json');
|
||||||
const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN';
|
|
||||||
|
|
||||||
// Claim game function
|
// Claim game function
|
||||||
const claimGame = async (page, game) => {
|
const claimGame = async (page, game) => {
|
||||||
|
|
@ -103,151 +101,56 @@ const claimGame = async (page, game) => {
|
||||||
return notify_game;
|
return notify_game;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure user is logged in - tries OAuth Device Flow first, falls back to browser login
|
// Check if logged in
|
||||||
const ensureLoggedIn = async (page, context) => {
|
const isLoggedIn = async page => {
|
||||||
const isLoggedIn = async () => await page.locator('egs-navigation').getAttribute('isloggedin') === 'true';
|
|
||||||
const user = cfg.eg_email || 'default';
|
|
||||||
|
|
||||||
// Try OAuth Device Flow first (bypasses Cloudflare)
|
|
||||||
let useDeviceFlow = true;
|
|
||||||
let accessToken = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
L.info('Attempting OAuth Device Flow login (Cloudflare bypass)');
|
const attr = await page.locator('egs-navigation').getAttribute('isloggedin');
|
||||||
|
return attr === 'true';
|
||||||
// Step 1: Try to get valid access token from stored device auth
|
} catch {
|
||||||
accessToken = await getValidAccessToken(user);
|
return false;
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
L.info('Using existing valid access token');
|
|
||||||
} else {
|
|
||||||
// Step 2: No valid token - start new device auth flow
|
|
||||||
L.info('No valid token found, starting device auth flow');
|
|
||||||
|
|
||||||
const { verificationUrl, userCode, expiresAt } = await startDeviceAuthLogin(user);
|
|
||||||
|
|
||||||
// Notify user with verification URL
|
|
||||||
const timeRemaining = Math.round((expiresAt - Date.now()) / 60000);
|
|
||||||
await notify(
|
|
||||||
`epic-games: Click here to login:<br><a href="${verificationUrl}">${verificationUrl}</a><br>` +
|
|
||||||
`User Code: <strong>${userCode}</strong><br>` +
|
|
||||||
`Expires in: ${timeRemaining} minutes`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`🔐 Device Auth URL: ${verificationUrl}`);
|
|
||||||
console.log(`🔐 User Code: ${userCode}`);
|
|
||||||
console.log(`⏰ Expires in: ${timeRemaining} minutes`);
|
|
||||||
|
|
||||||
// Wait for user to complete authorization
|
|
||||||
const interval = 5; // poll every 5 seconds
|
|
||||||
const authToken = await completeDeviceAuthLogin(
|
|
||||||
verificationUrl.split('userCode=')[1]?.split('&')[0] || '',
|
|
||||||
expiresAt,
|
|
||||||
interval,
|
|
||||||
);
|
|
||||||
|
|
||||||
accessToken = authToken.access_token;
|
|
||||||
L.info('Device auth completed successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Apply bearer token to browser
|
|
||||||
L.info('Applying access token to browser');
|
|
||||||
|
|
||||||
/** @type {import('playwright-firefox').Cookie} */
|
|
||||||
const bearerCookie = {
|
|
||||||
name: 'EPIC_BEARER_TOKEN',
|
|
||||||
value: accessToken,
|
|
||||||
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', timeout: cfg.timeout });
|
|
||||||
|
|
||||||
// Verify login worked
|
|
||||||
const loggedIn = await isLoggedIn();
|
|
||||||
if (loggedIn) {
|
|
||||||
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
|
||||||
L.info({ user: displayName }, 'Successfully logged in via Device Flow');
|
|
||||||
console.log(`✅ Signed in as ${displayName} (OAuth Device Flow)`);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
L.warn('Bearer token did not result in logged-in state');
|
|
||||||
useDeviceFlow = false;
|
|
||||||
} catch (err) {
|
|
||||||
L.warn({ err: err.message }, 'OAuth Device Flow failed, falling back to browser login');
|
|
||||||
console.log('⚠️ Device Auth failed:', err.message);
|
|
||||||
console.log('📝 Falling back to browser-based login with email/password...');
|
|
||||||
useDeviceFlow = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Browser-based login with email/password
|
|
||||||
if (!useDeviceFlow) {
|
|
||||||
L.info('Using browser-based login (email/password)');
|
|
||||||
|
|
||||||
// Check if already logged in (from cookies)
|
|
||||||
if (await isLoggedIn()) {
|
|
||||||
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
|
||||||
L.info({ user: displayName }, 'Already logged in (from cookies)');
|
|
||||||
console.log(`✅ Already signed in as ${displayName}`);
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try browser login
|
|
||||||
console.log('📝 Attempting browser login with email/password...');
|
|
||||||
const logged = await attemptBrowserLogin(page, context, isLoggedIn);
|
|
||||||
|
|
||||||
if (!logged) {
|
|
||||||
L.error('Browser login failed');
|
|
||||||
console.log('❌ Browser login failed. Please login manually.');
|
|
||||||
await notify('epic-games: Login failed. Please login manually in browser.');
|
|
||||||
|
|
||||||
if (cfg.headless) {
|
|
||||||
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
|
||||||
await context.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Waiting for manual login in browser...');
|
|
||||||
await page.waitForTimeout(cfg.login_timeout);
|
|
||||||
|
|
||||||
if (!await isLoggedIn()) {
|
|
||||||
throw new Error('Login did not complete within timeout');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
|
||||||
L.info({ user: displayName }, 'Successfully logged in via browser');
|
|
||||||
console.log(`✅ Signed in as ${displayName} (Browser Login)`);
|
|
||||||
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Login failed');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Browser-based login helper function
|
// Browser-based login with FlareSolverr support
|
||||||
const attemptBrowserLogin = async (page, context, isLoggedIn) => {
|
const attemptBrowserLogin = async (page, context) => {
|
||||||
if (!cfg.eg_email || !cfg.eg_password) {
|
if (!cfg.eg_email || !cfg.eg_password) {
|
||||||
L.warn('No email/password configured');
|
L.warn('No email/password configured');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
L.info({ email: cfg.eg_email }, 'Attempting browser login');
|
||||||
|
console.log('📝 Logging in with email/password...');
|
||||||
|
|
||||||
await page.goto(URL_LOGIN, {
|
await page.goto(URL_LOGIN, {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: cfg.login_timeout,
|
timeout: cfg.login_timeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check for Cloudflare and solve if needed
|
||||||
|
await page.waitForTimeout(2000); // Let page stabilize
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await isCloudflareChallenge(page)) {
|
||||||
|
L.warn('Cloudflare challenge detected during login');
|
||||||
|
console.log('☁️ Cloudflare detected, attempting to solve...');
|
||||||
|
|
||||||
|
if (cfg.flaresolverr_url) {
|
||||||
|
const solution = await solveCloudflare(page, URL_LOGIN);
|
||||||
|
if (solution) {
|
||||||
|
console.log('✅ Cloudflare solved by FlareSolverr');
|
||||||
|
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ FlareSolverr failed, may need manual solve');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ FlareSolverr not configured, may need manual solve');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'Cloudflare check failed');
|
||||||
|
}
|
||||||
|
|
||||||
const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first();
|
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 passwordField = page.locator('input[name="password"], input#password').first();
|
||||||
const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first();
|
const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first();
|
||||||
|
|
@ -256,19 +159,27 @@ const attemptBrowserLogin = async (page, context, isLoggedIn) => {
|
||||||
if (await emailField.count() > 0) {
|
if (await emailField.count() > 0) {
|
||||||
await emailField.fill(cfg.eg_email);
|
await emailField.fill(cfg.eg_email);
|
||||||
await continueBtn.click();
|
await continueBtn.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Password + submit
|
// Step 2: Password + submit
|
||||||
await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
|
try {
|
||||||
await passwordField.fill(cfg.eg_password);
|
await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
|
||||||
|
await passwordField.fill(cfg.eg_password);
|
||||||
|
|
||||||
const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
|
const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
|
||||||
if (await rememberMe.count() > 0) await rememberMe.check();
|
if (await rememberMe.count() > 0) await rememberMe.check();
|
||||||
await continueBtn.click();
|
await continueBtn.click();
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'Password field not found, may already be logged in');
|
||||||
|
return await isLoggedIn(page);
|
||||||
|
}
|
||||||
|
|
||||||
// MFA step
|
// MFA step
|
||||||
try {
|
try {
|
||||||
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
await page.waitForURL('**/id/login/mfa**', { timeout: 15000 });
|
||||||
|
console.log('🔐 2FA detected');
|
||||||
|
|
||||||
const otp = cfg.eg_otpkey
|
const otp = cfg.eg_otpkey
|
||||||
? authenticator.generate(cfg.eg_otpkey)
|
? authenticator.generate(cfg.eg_otpkey)
|
||||||
: await prompt({
|
: await prompt({
|
||||||
|
|
@ -289,20 +200,130 @@ const attemptBrowserLogin = async (page, context, isLoggedIn) => {
|
||||||
}
|
}
|
||||||
await continueBtn.click();
|
await continueBtn.click();
|
||||||
} catch {
|
} catch {
|
||||||
// No MFA
|
// No MFA required
|
||||||
|
L.trace('No MFA required');
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForURL('**/free-games', { timeout: cfg.login_timeout });
|
// Wait for successful login
|
||||||
return await isLoggedIn();
|
try {
|
||||||
|
await page.waitForURL('**/free-games**', { timeout: cfg.login_timeout });
|
||||||
|
L.info('Login successful');
|
||||||
|
return await isLoggedIn(page);
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'Login URL timeout, checking if logged in anyway');
|
||||||
|
return await isLoggedIn(page);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
L.error({ err }, 'Browser login failed');
|
L.error({ err }, 'Browser login failed');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ensure user is logged in
|
||||||
|
const ensureLoggedIn = async (page, context) => {
|
||||||
|
L.info('Checking login status');
|
||||||
|
|
||||||
|
// Check if already logged in (from saved cookies)
|
||||||
|
if (await isLoggedIn(page)) {
|
||||||
|
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
|
L.info({ user: displayName }, 'Already logged in (from cookies)');
|
||||||
|
console.log(`✅ Already signed in as ${displayName}`);
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
L.info('Not logged in, attempting login');
|
||||||
|
console.log('📝 Not logged in, starting login process...');
|
||||||
|
|
||||||
|
// Try browser login with email/password
|
||||||
|
const logged = await attemptBrowserLogin(page, context);
|
||||||
|
|
||||||
|
if (!logged) {
|
||||||
|
L.error('Browser login failed');
|
||||||
|
console.log('❌ Automatic login failed.');
|
||||||
|
|
||||||
|
// If headless, we can't do manual login
|
||||||
|
if (cfg.headless) {
|
||||||
|
const msg = 'Login failed in headless mode. Run with SHOW=1 to login manually via noVNC.';
|
||||||
|
console.error(msg);
|
||||||
|
await notify(`epic-games: ${msg}`);
|
||||||
|
throw new Error('Login failed, headless mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for manual login in visible browser
|
||||||
|
console.log('⏳ Waiting for manual login in browser...');
|
||||||
|
console.log(` Open noVNC at: http://localhost:${cfg.novnc_port || '6080'}`);
|
||||||
|
await notify(
|
||||||
|
'epic-games: Manual login required!<br>' +
|
||||||
|
`Open noVNC: <a href="http://localhost:${cfg.novnc_port || '6080'}">http://localhost:${cfg.novnc_port || '6080'}</a><br>` +
|
||||||
|
`Login timeout: ${cfg.login_timeout / 1000}s`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxWait = cfg.login_timeout;
|
||||||
|
const checkInterval = 5000;
|
||||||
|
let waited = 0;
|
||||||
|
|
||||||
|
while (waited < maxWait) {
|
||||||
|
await page.waitForTimeout(checkInterval);
|
||||||
|
waited += checkInterval;
|
||||||
|
|
||||||
|
if (await isLoggedIn(page)) {
|
||||||
|
L.info('Manual login detected');
|
||||||
|
console.log('✅ Manual login detected!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress update every 30 seconds
|
||||||
|
if (waited % 30000 === 0) {
|
||||||
|
const remaining = Math.round((maxWait - waited) / 1000);
|
||||||
|
console.log(` Still waiting... ${remaining}s remaining`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await isLoggedIn(page)) {
|
||||||
|
throw new Error('Manual login did not complete within timeout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
|
L.info({ user: displayName }, 'Successfully logged in');
|
||||||
|
console.log(`✅ Signed in as ${displayName}`);
|
||||||
|
|
||||||
|
return displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save cookies to file
|
||||||
|
const saveCookies = async context => {
|
||||||
|
try {
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
writeFileSync(COOKIES_PATH, JSON.stringify(cookies, null, 2));
|
||||||
|
L.trace({ cookieCount: cookies.length }, 'Cookies saved');
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'Failed to save cookies');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load cookies from file
|
||||||
|
const loadCookies = async context => {
|
||||||
|
if (!existsSync(COOKIES_PATH)) {
|
||||||
|
L.trace('No saved cookies found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cookies = JSON.parse(readFileSync(COOKIES_PATH, 'utf8'));
|
||||||
|
await context.addCookies(cookies);
|
||||||
|
L.info({ cookieCount: cookies.length }, 'Loaded saved cookies');
|
||||||
|
console.log('✅ Loaded saved cookies');
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
L.warn({ err: err.message }, 'Failed to load cookies');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Main function to claim Epic Games
|
// Main function to claim Epic Games
|
||||||
export const claimEpicGamesNew = async () => {
|
export const claimEpicGamesNew = async () => {
|
||||||
console.log('Starting Epic Games claimer (new mode, cookies + API)');
|
console.log('🚀 Starting Epic Games claimer (new mode)');
|
||||||
const db = await jsonDb('epic-games.json', {});
|
const db = await jsonDb('epic-games.json', {});
|
||||||
const notify_games = [];
|
const notify_games = [];
|
||||||
|
|
||||||
|
|
@ -322,71 +343,69 @@ export const claimEpicGamesNew = async () => {
|
||||||
const page = context.pages().length ? context.pages()[0] : await context.newPage();
|
const page = context.pages().length ? context.pages()[0] : await context.newPage();
|
||||||
await page.setViewportSize({ width: cfg.width, height: cfg.height });
|
await page.setViewportSize({ width: cfg.width, height: cfg.height });
|
||||||
|
|
||||||
// Load cookies from file if available
|
|
||||||
if (existsSync(COOKIES_PATH)) {
|
|
||||||
try {
|
|
||||||
const cookies = JSON.parse(readFileSync(COOKIES_PATH, 'utf8'));
|
|
||||||
await context.addCookies(cookies);
|
|
||||||
console.log('✅ Cookies loaded from file');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load cookies:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use device auths if available (from legacy mode)
|
|
||||||
const deviceAuths = await getAccountAuth();
|
|
||||||
if (deviceAuths && cfg.eg_email) {
|
|
||||||
const accountAuth = deviceAuths[cfg.eg_email];
|
|
||||||
if (accountAuth) {
|
|
||||||
console.log('🔄 Reusing device auth from legacy mode');
|
|
||||||
const cookies = [
|
|
||||||
{ name: 'EPIC_SSO_RM', value: accountAuth.deviceAuth?.refreshToken || '', domain: '.epicgames.com', path: '/' },
|
|
||||||
{ name: 'EPIC_DEVICE', value: accountAuth.deviceAuth?.deviceId || '', domain: '.epicgames.com', path: '/' },
|
|
||||||
{ name: 'EPIC_SESSION_AP', value: accountAuth.deviceAuth?.accountId || '', domain: '.epicgames.com', path: '/' },
|
|
||||||
];
|
|
||||||
await context.addCookies(cookies);
|
|
||||||
console.log('✅ Device auth cookies loaded');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Load saved cookies
|
||||||
|
await loadCookies(context);
|
||||||
|
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Ensure logged in
|
||||||
user = await ensureLoggedIn(page, context);
|
user = await ensureLoggedIn(page, context);
|
||||||
db.data[user] ||= {};
|
db.data[user] ||= {};
|
||||||
|
|
||||||
|
// Fetch free games
|
||||||
const freeGames = await fetchFreeGamesAPI(page);
|
const freeGames = await fetchFreeGamesAPI(page);
|
||||||
console.log('Free games via API:', freeGames.map(g => g.pageSlug));
|
console.log('🎮 Free games available:', freeGames.length);
|
||||||
|
if (freeGames.length > 0) {
|
||||||
|
console.log(' ' + freeGames.map(g => g.title).join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim each game
|
||||||
for (const game of freeGames) {
|
for (const game of freeGames) {
|
||||||
|
if (cfg.time) console.time('claim game');
|
||||||
|
|
||||||
const result = await claimGame(page, game);
|
const result = await claimGame(page, game);
|
||||||
notify_games.push(result);
|
notify_games.push(result);
|
||||||
|
|
||||||
db.data[user][game.offerId || game.pageSlug] = {
|
db.data[user][game.offerId || game.pageSlug] = {
|
||||||
title: game.title,
|
title: game.title,
|
||||||
time: datetime(),
|
time: datetime(),
|
||||||
url: `https://store.epicgames.com/${game.pageSlug}`,
|
url: `https://store.epicgames.com/${game.pageSlug}`,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (cfg.time) console.timeEnd('claim game');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save cookies to file
|
// Save cookies for next run
|
||||||
const cookies = await context.cookies();
|
await saveCookies(context);
|
||||||
writeFileSync(COOKIES_PATH, JSON.stringify(cookies, null, 2));
|
|
||||||
|
console.log('✅ Epic Games claimer completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.exitCode ||= 1;
|
process.exitCode ||= 1;
|
||||||
console.error('--- Exception (new epic):');
|
console.error('--- Exception:');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.message && process.exitCode !== 130) notify(`epic-games (new) failed: ${error.message.split('\n')[0]}`);
|
if (error.message && process.exitCode !== 130) {
|
||||||
|
notify(`epic-games (new) failed: ${error.message.split('\n')[0]}`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await db.write();
|
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) {
|
// Send notification if games were claimed or failed
|
||||||
console.log(JSON.stringify(await context.cookies(), null, 2));
|
if (notify_games.filter(g => g.status === 'claimed' || g.status === 'failed').length) {
|
||||||
|
notify(`epic-games (${user || 'unknown'}):<br>${html_game_list(notify_games)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.debug && context) {
|
||||||
|
console.log('Cookies:', JSON.stringify(await context.cookies(), null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.video()) {
|
||||||
|
console.log('Recorded video:', await page.video().path());
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.close();
|
||||||
}
|
}
|
||||||
await context.close();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue