feat: enhance new epic claimer with cookie persistence and oauth device flow
This commit is contained in:
parent
7a9f31df7c
commit
d05c184156
2 changed files with 175 additions and 116 deletions
|
|
@ -66,6 +66,7 @@ Common options:
|
||||||
- `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: `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.
|
||||||
- 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`)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { firefox } from 'playwright-firefox';
|
import { firefox } from 'playwright-firefox';
|
||||||
import { authenticator } from 'otplib';
|
import { authenticator } from 'otplib';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { resolve, 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';
|
||||||
|
|
||||||
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 COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json');
|
||||||
|
const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN';
|
||||||
|
|
||||||
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
|
||||||
|
|
||||||
|
|
@ -21,77 +24,174 @@ const fetchFreeGamesAPI = async () => {
|
||||||
const mapping = g.catalogNs?.mappings?.[0];
|
const mapping = g.catalogNs?.mappings?.[0];
|
||||||
return {
|
return {
|
||||||
title: g.title,
|
title: g.title,
|
||||||
namespace: mapping?.pageSlug ? mapping.id : g.catalogNs?.mappings?.[0]?.id,
|
namespace: mapping?.id || g.productSlug,
|
||||||
pageSlug: mapping?.pageSlug || g.urlSlug,
|
pageSlug: mapping?.pageSlug || g.urlSlug,
|
||||||
offerId: offer?.offerId,
|
offerId: offer?.offerId,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (e) {
|
||||||
|
if (e.response?.data?.error === 'authorization_pending') {
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('OAuth timeout');
|
||||||
|
};
|
||||||
|
|
||||||
|
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: '/' };
|
||||||
|
}) || [];
|
||||||
|
// also persist bearer token explicitly
|
||||||
|
cookies.push({ name: BEARER_TOKEN_NAME, value: accessToken, domain: '.epicgames.com', path: '/' });
|
||||||
|
return cookies;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValidAuth = async ({ email, password, 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)...');
|
||||||
|
const deviceResponse = await axios.post('https://api.epicgames.dev/epic/oauth/deviceCode', {
|
||||||
|
client_id: '34a02cf8f4414e29b159cdd02e6184bd',
|
||||||
|
scope: 'account.basicprofile account.userentitlements',
|
||||||
|
});
|
||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
const ensureLoggedIn = async (page, context) => {
|
const ensureLoggedIn = async (page, context) => {
|
||||||
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
|
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
|
||||||
console.error('Not signed in anymore. 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);
|
||||||
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_CLAIM, { waitUntil: 'domcontentloaded' });
|
||||||
if (cfg.eg_email && cfg.eg_password) console.info('Using email and password from environment.');
|
console.log('Waiting for manual login in the browser (cookies might be invalid).');
|
||||||
else console.info('Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).');
|
await notify('epic-games (new): please login in browser; cookies invalid or expired.');
|
||||||
|
if (cfg.headless) {
|
||||||
const notifyBrowserLogin = async () => {
|
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
||||||
console.log('Waiting for you to login in the browser.');
|
await context.close();
|
||||||
await notify('epic-games: no longer signed in and not enough options set for automatic login.');
|
process.exit(1);
|
||||||
if (cfg.headless) {
|
|
||||||
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
|
|
||||||
await context.close();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const email = cfg.eg_email || await prompt({ message: 'Enter email' });
|
|
||||||
if (!email) {
|
|
||||||
await notifyBrowserLogin();
|
|
||||||
await page.waitForURL(URL_CLAIM);
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
await page.waitForTimeout(cfg.login_timeout);
|
||||||
await page.fill('#email', email);
|
|
||||||
const password = cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' });
|
|
||||||
if (password) {
|
|
||||||
await page.fill('#password', password);
|
|
||||||
await page.click('button[type="submit"]');
|
|
||||||
} else {
|
|
||||||
await notifyBrowserLogin();
|
|
||||||
await page.waitForURL(URL_CLAIM);
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const watchMfaStep = async () => {
|
|
||||||
try {
|
|
||||||
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
|
|
||||||
console.log('Enter the security code to continue - security code sent to your email/device.');
|
|
||||||
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!' });
|
|
||||||
await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
|
|
||||||
await page.click('button[type="submit"]');
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
watchMfaStep();
|
|
||||||
|
|
||||||
await page.waitForURL(URL_CLAIM);
|
|
||||||
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
|
||||||
}
|
}
|
||||||
const user = await page.locator('egs-navigation').getAttribute('displayname');
|
const user = await page.locator('egs-navigation').getAttribute('displayname');
|
||||||
console.log(`Signed in as ${user}`);
|
console.log(`Signed in as ${user}`);
|
||||||
|
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 p = screenshot('failed', `${game.offerId || game.pageSlug}_${filenamify(datetime())}.png`);
|
||||||
|
await page.screenshot({ path: p, fullPage: true }).catch(() => {});
|
||||||
|
console.error(' Failed to claim:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notify_game;
|
||||||
|
};
|
||||||
|
|
||||||
export const claimEpicGamesNew = async () => {
|
export const claimEpicGamesNew = async () => {
|
||||||
console.log('Starting Epic Games claimer (new mode)');
|
console.log('Starting Epic Games claimer (new mode, cookies + API)');
|
||||||
const db = await jsonDb('epic-games.json', {});
|
const db = await jsonDb('epic-games.json', {});
|
||||||
|
const notify_games = [];
|
||||||
|
|
||||||
const freeGames = await fetchFreeGamesAPI();
|
const freeGames = await fetchFreeGamesAPI();
|
||||||
console.log('Free games via API:', freeGames.map(g => g.pageSlug));
|
console.log('Free games via API:', freeGames.map(g => g.pageSlug));
|
||||||
|
|
@ -113,90 +213,48 @@ 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 });
|
||||||
|
|
||||||
const notify_games = [];
|
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await context.addCookies([
|
const auth = await getValidAuth({
|
||||||
{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' },
|
email: cfg.eg_email,
|
||||||
{ name: 'HasAcceptedAgeGates', value: 'USK:9007199254740991,general:18,EPIC SUGGESTED RATING:18', domain: 'store.epicgames.com', path: '/' },
|
password: cfg.eg_password,
|
||||||
]);
|
otpKey: cfg.eg_otpkey,
|
||||||
|
reuseCookies: true,
|
||||||
|
cookiesPath: COOKIES_PATH,
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.addCookies(auth.cookies);
|
||||||
|
console.log('✅ Cookies loaded:', auth.cookies.length);
|
||||||
|
|
||||||
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
|
||||||
user = await ensureLoggedIn(page, context);
|
user = await ensureLoggedIn(page, context);
|
||||||
db.data[user] ||= {};
|
db.data[user] ||= {};
|
||||||
|
|
||||||
for (const game of freeGames) {
|
for (const game of freeGames) {
|
||||||
const purchaseUrl = `https://store.epicgames.com/purchase?namespace=${game.namespace}&offers=${game.offerId}`;
|
const result = await claimGame(page, game);
|
||||||
console.log('Processing', chalk.blue(game.title), purchaseUrl);
|
notify_games.push(result);
|
||||||
const notify_game = { title: game.title, url: purchaseUrl, status: 'failed' };
|
db.data[user][game.offerId || game.pageSlug] = {
|
||||||
notify_games.push(notify_game);
|
title: game.title,
|
||||||
|
time: datetime(),
|
||||||
await page.goto(purchaseUrl, { waitUntil: 'domcontentloaded' });
|
url: `https://store.epicgames.com/${game.pageSlug}`,
|
||||||
|
status: result.status,
|
||||||
const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"]').first();
|
};
|
||||||
await purchaseBtn.waitFor({ timeout: cfg.timeout });
|
|
||||||
const btnText = (await purchaseBtn.innerText()).toLowerCase();
|
|
||||||
|
|
||||||
if (btnText.includes('library')) {
|
|
||||||
console.log(' Already in library.');
|
|
||||||
notify_game.status = 'existed';
|
|
||||||
db.data[user][game.offerId] = { title: game.title, time: datetime(), url: purchaseUrl, status: 'existed' };
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cfg.dryrun) {
|
|
||||||
console.log(' DRYRUN=1 -> Skip order!');
|
|
||||||
notify_game.status = 'skipped';
|
|
||||||
db.data[user][game.offerId] = { title: game.title, time: datetime(), url: purchaseUrl, status: 'skipped' };
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await purchaseBtn.click({ delay: 10 });
|
|
||||||
await page.waitForSelector('#webPurchaseContainer iframe');
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
|
|
||||||
const btnAgree = iframe.locator('button:has-text("I Accept")');
|
|
||||||
try {
|
|
||||||
await btnAgree.waitFor({ timeout: 10000 });
|
|
||||||
await btnAgree.click();
|
|
||||||
} catch {
|
|
||||||
// not required
|
|
||||||
}
|
|
||||||
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached', timeout: cfg.timeout });
|
|
||||||
notify_game.status = 'claimed';
|
|
||||||
db.data[user][game.offerId] = { title: game.title, time: datetime(), url: purchaseUrl, status: 'claimed' };
|
|
||||||
console.log(' Claimed successfully!');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(' Failed to claim:', e.message);
|
|
||||||
notify_game.status = 'failed';
|
|
||||||
db.data[user][game.offerId] = { title: game.title, time: datetime(), url: purchaseUrl, status: 'failed' };
|
|
||||||
const p = screenshot('failed', `${game.offerId}_${filenamify(datetime())}.png`);
|
|
||||||
await page.screenshot({ path: p, fullPage: true }).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await writeFileSync(COOKIES_PATH, JSON.stringify(await context.cookies(), null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
process.exitCode ||= 1;
|
process.exitCode ||= 1;
|
||||||
console.error('--- Exception:');
|
console.error('--- Exception (new epic):');
|
||||||
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) {
|
if (notify_games.filter(g => g.status == 'claimed' || g.status == 'failed').length) {
|
||||||
notify(`epic-games (new ${user}):<br>${html_game_list(notify_games)}`);
|
notify(`epic-games (new ${user || 'unknown'}):<br>${html_game_list(notify_games)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.debug && context) {
|
if (cfg.debug && context) {
|
||||||
console.log(JSON.stringify(await context.cookies(), null, 2));
|
console.log(JSON.stringify(await context.cookies(), null, 2));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue