diff --git a/OAUTH_DEVICE_FLOW_ISSUE.md b/OAUTH_DEVICE_FLOW_ISSUE.md
new file mode 100644
index 0000000..5d4ed12
--- /dev/null
+++ b/OAUTH_DEVICE_FLOW_ISSUE.md
@@ -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
diff --git a/README.md b/README.md
index f5e92dd..3f1cfdd 100644
--- a/README.md
+++ b/README.md
@@ -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).
+---
+
+## 📖 Complete Setup Guide
+
+For detailed setup instructions, see **[SETUP.md](SETUP.md)**.
+
+---
+
## Troubleshooting
### Cloudflare / "Incorrect response" Error (Epic Games)
diff --git a/SETUP.md b/SETUP.md
new file mode 100644
index 0000000..b4c5439
--- /dev/null
+++ b/SETUP.md
@@ -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
diff --git a/epic-claimer-new.js b/epic-claimer-new.js
index 74ac415..54180c0 100644
--- a/epic-claimer-new.js
+++ b/epic-claimer-new.js
@@ -13,11 +13,10 @@ import {
handleSIGINT,
} from './src/util.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 { getAccountAuth, setAccountAuth } from './src/device-auths.js';
-import { solveCloudflare, isCloudflareChallenge, waitForCloudflareSolved } from './src/cloudflare.js';
-import { getValidAccessToken, startDeviceAuthLogin, completeDeviceAuthLogin, refreshDeviceAuth } from './src/device-login.js';
+import { solveCloudflare, isCloudflareChallenge } from './src/cloudflare.js';
import logger from './src/logger.js';
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_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 BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN';
// Claim game function
const claimGame = async (page, game) => {
@@ -103,151 +101,56 @@ const claimGame = async (page, game) => {
return notify_game;
};
-// Ensure user is logged in - tries OAuth Device Flow first, falls back to browser login
-const ensureLoggedIn = async (page, context) => {
- 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;
-
+// Check if logged in
+const isLoggedIn = async page => {
try {
- L.info('Attempting OAuth Device Flow login (Cloudflare bypass)');
-
- // Step 1: Try to get valid access token from stored device auth
- accessToken = await getValidAccessToken(user);
-
- 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:
${verificationUrl}
` +
- `User Code: ${userCode}
` +
- `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;
+ const attr = await page.locator('egs-navigation').getAttribute('isloggedin');
+ return attr === 'true';
+ } catch {
+ return 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
-const attemptBrowserLogin = async (page, context, isLoggedIn) => {
+// Browser-based login with FlareSolverr support
+const attemptBrowserLogin = async (page, context) => {
if (!cfg.eg_email || !cfg.eg_password) {
L.warn('No email/password configured');
return false;
}
try {
+ L.info({ email: cfg.eg_email }, 'Attempting browser login');
+ console.log('📝 Logging in with email/password...');
+
await page.goto(URL_LOGIN, {
waitUntil: 'domcontentloaded',
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 passwordField = page.locator('input[name="password"], input#password').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) {
await emailField.fill(cfg.eg_email);
await continueBtn.click();
+ await page.waitForTimeout(1000);
}
// Step 2: Password + submit
- await passwordField.waitFor({ timeout: cfg.login_visible_timeout });
- await passwordField.fill(cfg.eg_password);
+ try {
+ 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();
+ const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first();
+ if (await rememberMe.count() > 0) await rememberMe.check();
+ 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
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
? authenticator.generate(cfg.eg_otpkey)
: await prompt({
@@ -289,20 +200,130 @@ const attemptBrowserLogin = async (page, context, isLoggedIn) => {
}
await continueBtn.click();
} catch {
- // No MFA
+ // No MFA required
+ L.trace('No MFA required');
}
- await page.waitForURL('**/free-games', { timeout: cfg.login_timeout });
- return await isLoggedIn();
+ // Wait for successful login
+ 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) {
L.error({ err }, 'Browser login failed');
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!
' +
+ `Open noVNC: http://localhost:${cfg.novnc_port || '6080'}
` +
+ `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
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 notify_games = [];
@@ -322,71 +343,69 @@ export const claimEpicGamesNew = async () => {
const page = context.pages().length ? context.pages()[0] : await context.newPage();
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;
try {
+ // Load saved cookies
+ await loadCookies(context);
+
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
+
+ // Ensure logged in
user = await ensureLoggedIn(page, context);
db.data[user] ||= {};
+ // Fetch free games
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) {
+ if (cfg.time) console.time('claim game');
+
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,
};
+
+ if (cfg.time) console.timeEnd('claim game');
}
- // Save cookies to file
- const cookies = await context.cookies();
- writeFileSync(COOKIES_PATH, JSON.stringify(cookies, null, 2));
+ // Save cookies for next run
+ await saveCookies(context);
+
+ console.log('✅ Epic Games claimer completed');
} catch (error) {
process.exitCode ||= 1;
- console.error('--- Exception (new epic):');
+ console.error('--- Exception:');
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 {
await db.write();
- if (notify_games.filter(g => g.status === 'claimed' || g.status === 'failed').length) {
- notify(`epic-games (new ${user || 'unknown'}):
${html_game_list(notify_games)}`);
- }
- }
- if (cfg.debug && context) {
- console.log(JSON.stringify(await context.cookies(), null, 2));
+ // Send notification if games were claimed or failed
+ if (notify_games.filter(g => g.status === 'claimed' || g.status === 'failed').length) {
+ notify(`epic-games (${user || 'unknown'}):
${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();
};