mv {config,migrate,util,version}.js src/
This commit is contained in:
parent
6b9420804b
commit
64676795d1
10 changed files with 11 additions and 11 deletions
52
src/config.js
Normal file
52
src/config.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import { dataDir } from './util.js';
|
||||
|
||||
dotenv.config({ path: 'data/config.env' }); // loads env vars from file - will not set vars that are already set, i.e., can overwrite values from file by prefixing, e.g., VAR=VAL node ...
|
||||
|
||||
// Options - also see table in README.md
|
||||
export const cfg = {
|
||||
debug: process.env.DEBUG == '1' || process.env.PWDEBUG == '1', // runs non-headless and opens https://playwright.dev/docs/inspector
|
||||
debug_network: process.env.DEBUG_NETWORK == '1', // log network requests and responses
|
||||
record: process.env.RECORD == '1', // `recordHar` (network) + `recordVideo`
|
||||
time: process.env.TIME == '1', // log duration of each step
|
||||
dryrun: process.env.DRYRUN == '1', // don't claim anything
|
||||
interactive: process.env.INTERACTIVE == '1', // confirm to claim, default skip
|
||||
show: process.env.SHOW == '1', // run non-headless
|
||||
get headless() {
|
||||
return !this.debug && !this.show;
|
||||
},
|
||||
width: Number(process.env.WIDTH) || 1920, // width 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
|
||||
login_timeout: (Number(process.env.LOGIN_TIMEOUT) || 180) * 1000, // higher timeout for login, will wait twice: prompt + wait for manual login
|
||||
novnc_port: process.env.NOVNC_PORT, // running in docker if set
|
||||
notify: process.env.NOTIFY, // apprise notification services
|
||||
notify_title: process.env.NOTIFY_TITLE, // apprise notification title
|
||||
get dir() { // avoids ReferenceError: Cannot access 'dataDir' before initialization
|
||||
return {
|
||||
browser: process.env.BROWSER_DIR || dataDir('browser'), // for multiple accounts or testing
|
||||
screenshots: process.env.SCREENSHOTS_DIR || dataDir('screenshots'), // set to 0 to disable screenshots
|
||||
};
|
||||
},
|
||||
// auth epic-games
|
||||
eg_email: process.env.EG_EMAIL || process.env.EMAIL,
|
||||
eg_password: process.env.EG_PASSWORD || process.env.PASSWORD,
|
||||
eg_otpkey: process.env.EG_OTPKEY,
|
||||
eg_parentalpin: process.env.EG_PARENTALPIN,
|
||||
// auth prime-gaming
|
||||
pg_email: process.env.PG_EMAIL || process.env.EMAIL,
|
||||
pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,
|
||||
pg_otpkey: process.env.PG_OTPKEY,
|
||||
// auth gog
|
||||
gog_email: process.env.GOG_EMAIL || process.env.EMAIL,
|
||||
gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD,
|
||||
gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game
|
||||
// OTP only via GOG_EMAIL, can't add app...
|
||||
// auth xbox
|
||||
xbox_email: process.env.XBOX_EMAIL || process.env.EMAIL,
|
||||
xbox_password: process.env.XBOX_PASSWORD || process.env.PASSWORD,
|
||||
xbox_otpkey: process.env.XBOX_OTPKEY,
|
||||
// experimmental - likely to change
|
||||
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
|
||||
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
|
||||
};
|
||||
33
src/migrate.js
Normal file
33
src/migrate.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { existsSync } from 'fs';
|
||||
import { Low } from 'lowdb';
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { datetime } from './util.js';
|
||||
|
||||
const datetime_UTCtoLocalTimezone = async file => {
|
||||
if (!existsSync(file)) return console.error('File does not exist:', file);
|
||||
const db = new Low(new JSONFile(file));
|
||||
await db.read();
|
||||
db.data ||= {};
|
||||
console.log('Migrating', file);
|
||||
for (const user in db.data) {
|
||||
for (const game in db.data[user]) {
|
||||
const time1 = db.data[user][game].time;
|
||||
const time1s = time1.endsWith('Z') ? time1 : time1 + ' UTC';
|
||||
const time2 = datetime(new Date(time1s));
|
||||
console.log([game, time1, time2]);
|
||||
db.data[user][game].time = time2;
|
||||
}
|
||||
}
|
||||
// console.log(db.data);
|
||||
await db.write(); // write out json db
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args[0] == 'localtime') {
|
||||
const files = args.slice(1);
|
||||
console.log('Will convert UTC datetime to local timezone for', files);
|
||||
files.forEach(datetime_UTCtoLocalTimezone);
|
||||
} else {
|
||||
console.log('Usage: node migrate.js <cmd> <args>');
|
||||
console.log(' node migrate.js localtime data/*.json');
|
||||
}
|
||||
138
src/util.js
Normal file
138
src/util.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// not the same since these will give the absolute paths for this file instead of for the file using them
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
// explicit object instead of Object.fromEntries since the built-in type would loose the keys, better type: https://dev.to/svehla/typescript-object-fromentries-389c
|
||||
export const dataDir = s => path.resolve(__dirname, '..', 'data', s);
|
||||
|
||||
// modified path.resolve to return null if first argument is '0', used to disable screenshots
|
||||
export const resolve = (...a) => a.length && a[0] == '0' ? null : path.resolve(...a);
|
||||
|
||||
// json database
|
||||
import { JSONPreset } from 'lowdb/node';
|
||||
export const jsonDb = (file, defaultData) => JSONPreset(dataDir(file), defaultData);
|
||||
|
||||
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
// date and time as UTC (no timezone offset) in nicely readable and sortable format, e.g., 2022-10-06 12:05:27.313
|
||||
export const datetimeUTC = (d = new Date()) => d.toISOString().replace('T', ' ').replace('Z', '');
|
||||
// same as datetimeUTC() but for local timezone, e.g., UTC + 2h for the above in DE
|
||||
export const datetime = (d = new Date()) => datetimeUTC(new Date(d.getTime() - d.getTimezoneOffset() * 60000));
|
||||
export const filenamify = s => s.replaceAll(':', '.').replace(/[^a-z0-9 _\-.]/gi, '_'); // alternative: https://www.npmjs.com/package/filenamify - On Unix-like systems, / is reserved. On Windows, <>:"/\|?* along with trailing periods are reserved.
|
||||
|
||||
export const handleSIGINT = (context = null) => process.on('SIGINT', async () => { // e.g. when killed by Ctrl-C
|
||||
console.error('\nInterrupted by SIGINT. Exit!'); // Exception shows where the script was:\n'); // killed before catch in docker...
|
||||
process.exitCode = 130; // 128+SIGINT to indicate to parent that process was killed
|
||||
if (context) await context.close(); // in order to save recordings also on SIGINT, we need to disable Playwright's handleSIGINT and close the context ourselves
|
||||
});
|
||||
|
||||
export const launchChromium = async options => {
|
||||
const { chromium } = await import('playwright-chromium'); // stealth plugin needs no outdated playwright-extra
|
||||
|
||||
// https://www.nopecha.com extension source from https://github.com/NopeCHA/NopeCHA/releases/tag/0.1.16
|
||||
// const ext = path.resolve('nopecha'); // used in Chromium, currently not needed in Firefox
|
||||
|
||||
const context = chromium.launchPersistentContext(cfg.dir.browser, {
|
||||
// chrome will not work in linux arm64, only chromium
|
||||
// channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge
|
||||
args: [ // https://peter.sh/experiments/chromium-command-line-switches
|
||||
// don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.'
|
||||
// '--restore-last-session', // does not apply for crash/killed
|
||||
'--hide-crash-restore-bubble',
|
||||
// `--disable-extensions-except=${ext}`,
|
||||
// `--load-extension=${ext}`,
|
||||
],
|
||||
// ignoreDefaultArgs: ['--enable-automation'], // remove default arg that shows the info bar with 'Chrome is being controlled by automated test software.'. Since Chromeium 106 this leads to show another info bar with 'You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer.'.
|
||||
...options,
|
||||
});
|
||||
return context;
|
||||
};
|
||||
|
||||
export const stealth = async context => {
|
||||
// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212
|
||||
// https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions
|
||||
const enabledEvasions = [
|
||||
'chrome.app',
|
||||
'chrome.csi',
|
||||
'chrome.loadTimes',
|
||||
'chrome.runtime',
|
||||
// 'defaultArgs',
|
||||
'iframe.contentWindow',
|
||||
'media.codecs',
|
||||
'navigator.hardwareConcurrency',
|
||||
'navigator.languages',
|
||||
'navigator.permissions',
|
||||
'navigator.plugins',
|
||||
// 'navigator.vendor',
|
||||
'navigator.webdriver',
|
||||
'sourceurl',
|
||||
// 'user-agent-override', // doesn't work since playwright has no page.browser()
|
||||
'webgl.vendor',
|
||||
'window.outerdimensions',
|
||||
];
|
||||
const stealth = {
|
||||
callbacks: [],
|
||||
async evaluateOnNewDocument(...args) {
|
||||
this.callbacks.push({ cb: args[0], a: args[1] });
|
||||
},
|
||||
};
|
||||
for (const e of enabledEvasions) {
|
||||
const evasion = await import(`puppeteer-extra-plugin-stealth/evasions/${e}/index.js`);
|
||||
evasion.default().onPageCreated(stealth);
|
||||
}
|
||||
for (let evasion of stealth.callbacks) {
|
||||
await context.addInitScript(evasion.cb, evasion.a);
|
||||
}
|
||||
};
|
||||
|
||||
// used prompts before, but couldn't cancel prompt
|
||||
// alternative inquirer is big (node_modules 29MB, enquirer 9.7MB, prompts 9.8MB, none 9.4MB) and slower
|
||||
// open issue: prevents handleSIGINT() to work if prompt is cancelled with Ctrl-C instead of Escape: https://github.com/enquirer/enquirer/issues/372
|
||||
import Enquirer from 'enquirer'; const enquirer = new Enquirer();
|
||||
const timeoutPlugin = timeout => enquirer => { // cancel prompt after timeout ms
|
||||
enquirer.on('prompt', prompt => {
|
||||
const t = setTimeout(() => {
|
||||
prompt.hint = () => 'timeout';
|
||||
prompt.cancel();
|
||||
}, timeout);
|
||||
prompt.on('submit', _ => clearTimeout(t));
|
||||
prompt.on('cancel', _ => clearTimeout(t));
|
||||
});
|
||||
};
|
||||
enquirer.use(timeoutPlugin(cfg.login_timeout)); // TODO may not want to have this timeout for all prompts; better extend Prompt and add a timeout prompt option
|
||||
// single prompt that just returns the non-empty value instead of an object
|
||||
// @ts-ignore
|
||||
export const prompt = o => enquirer.prompt({ name: 'name', type: 'input', message: 'Enter value', ...o }).then(r => r.name).catch(_ => {});
|
||||
export const confirm = o => prompt({ type: 'confirm', message: 'Continue?', ...o });
|
||||
|
||||
// notifications via apprise CLI
|
||||
import { execFile } from 'child_process';
|
||||
import { cfg } from './config.js';
|
||||
|
||||
export const notify = html => new Promise((resolve, reject) => {
|
||||
if (!cfg.notify) {
|
||||
if (cfg.debug) console.debug('notify: NOTIFY is not set!');
|
||||
return resolve();
|
||||
}
|
||||
// const cmd = `apprise '${cfg.notify}' ${title} -i html -b '${html}'`; // this had problems if e.g. ' was used in arg; could have `npm i shell-escape`, but instead using safer execFile which takes args as array instead of exec which spawned a shell to execute the command
|
||||
const args = [cfg.notify, '-i', 'html', '-b', html];
|
||||
if (cfg.notify_title) args.push(...['-t', cfg.notify_title]);
|
||||
if (cfg.debug) console.debug(`apprise ${args.map(a => `'${a}'`).join(' ')}`); // this also doesn't escape, but it's just for info
|
||||
execFile('apprise', args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`);
|
||||
if (error.message.includes('command not found')) {
|
||||
console.info('Run `pip install apprise`. See https://github.com/vogler/free-games-claimer#notifications');
|
||||
}
|
||||
return reject(error);
|
||||
}
|
||||
if (stderr) console.error(`stderr: ${stderr}`);
|
||||
if (stdout) console.log(`stdout: ${stdout}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
export const escapeHtml = unsafe => unsafe.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('\'', ''');
|
||||
|
||||
export const html_game_list = games => games.map(g => `- <a href="${g.url}">${escapeHtml(g.title)}</a> (${g.status})`).join('<br>');
|
||||
49
src/version.js
Normal file
49
src/version.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// check if running the latest version
|
||||
|
||||
import { log } from 'console';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const execp = cmd => new Promise((resolve, reject) => {
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
if (stderr) console.error(`stderr: ${stderr}`);
|
||||
// if (stdout) console.log(`stdout: ${stdout}`);
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`);
|
||||
if (error.message.includes('command not found')) {
|
||||
console.info('Install git to check for updates!');
|
||||
}
|
||||
return reject(error);
|
||||
}
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
|
||||
// const git_main = () => readFileSync('.git/refs/heads/main').toString().trim();
|
||||
|
||||
let sha, date;
|
||||
// if (existsSync('/.dockerenv')) { // did not work
|
||||
if (process.env.NOVNC_PORT) {
|
||||
log('Running inside Docker.');
|
||||
['COMMIT', 'BRANCH', 'NOW'].forEach(v => log(` ${v}:`, process.env[v]));
|
||||
sha = process.env.COMMIT;
|
||||
date = process.env.NOW;
|
||||
} else {
|
||||
log('Not running inside Docker.');
|
||||
sha = await execp('git rev-parse HEAD');
|
||||
date = await execp('git show -s --format=%cD'); // same as format as `date -R` (RFC2822)
|
||||
// date = await execp('git show -s --format=%ch'); // %ch is same as --date=human (short/relative)
|
||||
}
|
||||
|
||||
const gh = await (await fetch('https://api.github.com/repos/vogler/free-games-claimer/commits/main', {
|
||||
// headers: { accept: 'application/vnd.github.VERSION.sha' }
|
||||
})).json();
|
||||
// log(gh);
|
||||
|
||||
log('Local commit:', sha, new Date(date));
|
||||
log('Online commit:', gh.sha, new Date(gh.commit.committer.date));
|
||||
|
||||
if (sha == gh.sha) {
|
||||
log('Running the latest version!');
|
||||
} else {
|
||||
log('Not running the latest version!');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue