diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c44241 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc.json b/.eslintrc.json index f6e7296..c3e26e2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,28 +1,34 @@ { "env": { - "browser": true, "node": true, - "es6": true + "es6": true, + "es2017": true, + "es2020": true, + "es2021": true }, "extends": "eslint:recommended", + "parser": "@babel/eslint-parser", "parserOptions": { - "ecmaVersion": 2018 + "requireConfigFile": false }, "rules": { "accessor-pairs": "error", - "array-bracket-newline": ["error", "consistent"], + "array-bracket-newline": ["error", { "multiline": true }], "array-bracket-spacing": ["error", "never", { "arraysInArrays": true }], "array-callback-return": "error", - "array-element-newline": ["error", "consistent"], + "array-element-newline": ["error", { + "ArrayExpression": "consistent", + "ArrayPattern": { "multiline": true } + }], "arrow-body-style": "error", - "arrow-parens": ["error", "as-needed"], + "arrow-parens": "error", "arrow-spacing": "error", "block-scoped-var": "error", "block-spacing": "error", "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "camelcase": "error", "class-methods-use-this": "error", - "comma-dangle": ["error", "only-multiline"], + "comma-dangle": ["error", "always-multiline"], "comma-spacing": "error", "comma-style": "error", "complexity": "error", @@ -46,7 +52,7 @@ "implicit-arrow-linebreak": "error", "indent": ["error", 4, { "SwitchCase": 1 }], "init-declarations": "error", - "key-spacing": ["error", { "mode": "minimum", "align": "value" }], + "key-spacing": ["error", { "align": "value" }], "keyword-spacing": "error", "linebreak-style": "error", "lines-between-class-members": ["error", "always"], @@ -67,6 +73,7 @@ "no-dupe-else-if": "error", "no-duplicate-imports": "error", "no-else-return": "error", + "no-extra-parens": ["error", "all", { "nestedBinaryExpressions": false }], "no-empty-function": "error", "no-eq-null": "error", "no-eval": "error", @@ -93,7 +100,6 @@ "no-new-wrappers": "error", "no-octal": "error", "no-octal-escape": "error", - "no-param-reassign": "error", "no-proto": "error", "no-redeclare": "error", "no-return-assign": "error", @@ -122,7 +128,7 @@ "no-with": "error", "no-whitespace-before-property": "error", "nonblock-statement-body-position": "error", - "object-curly-newline": "error", + "object-curly-newline": ["error", { "multiline": true }], "object-curly-spacing": ["error", "always"], "object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }], "object-shorthand": "error", @@ -159,7 +165,6 @@ "space-infix-ops": "error", "space-unary-ops": "error", "spaced-comment": "error", - "strict": ["error", "global"], "switch-colon-spacing": "error", "symbol-description": "error", "template-curly-spacing": "error", @@ -170,4 +175,4 @@ "yield-star-spacing": "error", "yoda": "error" } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 46738e2..046e1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.lnk node_modules package-lock.json +DeviceAuthGenerator.exe +device_auths.json .egstore/* .vscode/* !.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3dc3d05 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "eslint.format.enable": true, + "editor.formatOnSave": true, + "eslint.alwaysShowStatus": true, + "prettier.requireConfig": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } +} \ No newline at end of file diff --git a/README.md b/README.md index b55da2a..3f16f18 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,15 @@ Claim [available free game promotions](https://www.epicgames.com/store/free-games) from the Epic Game Store. ## Requirements + * [DeviceAuthGenerator](https://github.com/xMistt/DeviceAuthGenerator) * [Node.js](https://nodejs.org/download/) -## Instructions (arguments) -1. Download/clone repo -3. Run `npm install` -4. Run `npm start USERNAME PASSWORD 0|1 2FA_SECRET`* - -## Instructions (config) -1. Download/clone repo +## Instructions +1. Download/clone this repository 2. Run `npm install` -3. Edit `config.json` to include your EpicGames credentials and options -4. Run `npm start`* - -*Only this step is required after the initial use. +3. Generate `device_auths.json` (using [DeviceAuthGenerator](https://github.com/xMistt/DeviceAuthGenerator)) +4. (Optional) Edit `config.json` +5. Run `npm start` ## FAQ ### Why should I use this? @@ -28,10 +23,14 @@ Also, this is a good alternative, in case you don't like using Epic's client or ### Why should I even bother claiming these free games? To which I will say, why not? Most of these games are actually outstanding games! Even if you don't like Epic and their shenanigans, you will be pleased to know that Epic actually funds all the free copies that are given away: ["But we actually found it was more economical to pay developers [a lump sum] to distribute their game free for two weeks..."](https://arstechnica.com/gaming/2019/03/epic-ceo-youre-going-to-see-lower-prices-on-epic-games-store/) -### Can I use the looping or multi-account feature when using launch arguments? -No, these are only usable by using the config. - ## Changelog +### V1.5.0 + * Fixed login + * Fixed purchase (claiming) + * Removed ownership check (broken) + * Removed unneeded dependencies + * Code restyling + ### V1.4.1 * Removed the need for graphql query diff --git a/claimer.js b/claimer.js index 2f68bd7..04b964d 100644 --- a/claimer.js +++ b/claimer.js @@ -1,14 +1,13 @@ "use strict"; + const { "Launcher": EpicGames } = require("epicgames-client"); +const { freeGamesPromotions } = require("./src/gamePromotions"); + +const Auths = require(`${__dirname}/device_auths.json`); const CheckUpdate = require("check-update-github"); -const ClientLoginAdapter = require("epicgames-client-login-adapter"); const Config = require(`${__dirname}/config.json`); const Logger = require("tracer").console(`${__dirname}/logger.js`); const Package = require("./package.json"); -const TwoFactor = require("node-2fa"); -const Cookie = require('tough-cookie').Cookie; - -const { freeGamesPromotions } = require('./src/gamePromotions'); function isUpToDate() { return new Promise((res, rej) => { @@ -16,7 +15,7 @@ function isUpToDate() { "name": Package.name, "currentVersion": Package.version, "user": "revadike", - "branch": "master" + "branch": "master", }, (err, latestVersion) => { if (err) { rej(err); @@ -27,22 +26,8 @@ function isUpToDate() { }); } -function getChromeCookie(cookie) { - cookie = Object.assign({}, cookie); - cookie.name = cookie.key; - if (cookie.expires instanceof Date) { - cookie.expires = cookie.expires.getTime() / 1000.0; - } else { - delete cookie.expires; - } - return cookie; -} - -function getToughCookie(cookie) { - cookie = Object.assign({}, cookie); - cookie.key = cookie.name; - cookie.expires = new Date(cookie.expires * 1000); - return new Cookie(cookie); +function sleep(delay) { + return new Promise((res) => setTimeout(res, delay * 60000)); } (async() => { @@ -50,93 +35,27 @@ function getToughCookie(cookie) { Logger.warn(`There is a new version available: ${Package.url}`); } - let { accounts, options, delay, loop } = Config; - if (!options) { - options = {}; - } - let sleep = delay => new Promise(res => setTimeout(res, delay * 60000)); + let { options, delay, loop } = Config; do { - if (process.argv.length > 2) { - loop = false; - accounts = [{ - "email": process.argv[2], - "password": process.argv[3], - "rememberLastSession": Boolean(Number(process.argv[4])), - "secret": process.argv[5], - }]; - } - - for (let account of accounts) { - let noSecret = !account.secret || account.secret.length === 0; - if (!noSecret) { - let { token } = TwoFactor.generateToken(account.secret); - account.twoFactorCode = token; - } - - let epicOptions = Object.assign({}, options); - Object.assign(epicOptions, account); - - let client = new EpicGames(epicOptions); - + for (let email in Auths) { + let useDeviceAuth = true; + let clientOptions = { email, ...options }; + let client = new EpicGames(clientOptions); if (!await client.init()) { throw new Error("Error while initialize process."); } - let success = false; - try { - success = await client.login(account); - } catch (error) { - Logger.warn(error.message); - } - + let success = await client.login({ useDeviceAuth }); if (!success) { - Logger.warn(`Failed to login as ${client.config.email}, please attempt manually.`); - - - if (account.rememberLastSession) { - if (!options.cookies) { - options.cookies = []; - } - if (account.cookies && account.cookies.length) { - options.cookies = options.cookies.concat(account.cookies); - } - client.http.jar._jar.store.getAllCookies((err, cookies) => { - for (const cookie of cookies) { - options.cookies.push(getChromeCookie(cookie)); - } - }); - } - - let auth = await ClientLoginAdapter.init(account, options); - let exchangeCode = await auth.getExchangeCode(); - - if (account.rememberLastSession) { - let cookies = await auth.getPage().then(p => p.cookies()); - for (let cookie of cookies) { - cookie = getToughCookie(cookie); - client.http.jar.setCookie(cookie, "https://" + cookie.domain); - } - } - - await auth.close(); - - if (!await client.login(null, exchangeCode)) { - throw new Error("Error while logging in."); - } + throw new Error(`Failed to login as ${client.config.email}`); } Logger.info(`Logged in as ${client.account.name} (${client.account.id})`); - let { country } = client.account.country; + let { country } = client.account; let freePromos = await freeGamesPromotions(client, country, country); for (let offer of freePromos) { - let launcherQuery = await client.launcherQuery(offer.namespace, offer.id); - if (launcherQuery.data.Launcher.entitledOfferItems.entitledToAllItemsInOffer) { - Logger.info(`${offer.title} is already claimed for this account`); - continue; - } - try { let purchased = await client.purchase(offer, 1); if (purchased) { @@ -167,7 +86,7 @@ function getToughCookie(cookie) { process.exit(0); } } while (loop); -})().catch(err => { +})().catch((err) => { Logger.error(err); process.exit(1); }); diff --git a/config.json b/config.json index 35b6db9..b10cbe7 100644 --- a/config.json +++ b/config.json @@ -1,19 +1,4 @@ { - "accounts": [ - { - "email": "YOUR_EMAIL1_HERE", - "password": "YOUR_PASSWORD1_HERE", - "secret": "YOUR_2FA_SECRET1_HERE_OR_EMPTY", - "rememberLastSession": true - }, - { - "email": "YOUR_EMAIL2_HERE", - "password": "YOUR_PASSWORD2_HERE", - "secret": "YOUR_2FA_SECRET2_HERE_OR_EMPTY", - "rememberLastSession": true, - "cookies": [] - } - ], "options": {}, "delay": 1440, "loop": true diff --git a/package.json b/package.json index 7f2df19..4357226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicgames-freebies-claimer", - "version": "1.4.1", + "version": "1.5.0", "description": "Claim free game promotions from the Epic Game Store", "author": { "name": "Revadike", @@ -28,16 +28,15 @@ "homepage": "https://github.com/revadike/epicgames-freebies-claimer", "url": "https://github.com/revadike/epicgames-freebies-claimer", "devDependencies": { + "@babel/eslint-parser": "latest", "chai": "*", - "eslint": "^6.8.0", + "eslint": "latest", "mocha": "*" }, "dependencies": { "check-update-github": "^0.0.4", "colors": "^1.4.0", "epicgames-client": "Revadike/node-epicgames-client#develop", - "epicgames-client-login-adapter": "Revadike/node-epicgames-client-login-adapter", - "node-2fa": "^1.1.2", "tracer": "^1.0.3" } } diff --git a/src/gamePromotions.js b/src/gamePromotions.js index 713175a..7722135 100644 --- a/src/gamePromotions.js +++ b/src/gamePromotions.js @@ -1,32 +1,35 @@ +"use strict"; async function freeGamesPromotions(client, country = "US", allowCountries = "US", locale = "en-US") { let { data } = await client.freeGamesPromotions(country, allowCountries, locale); let { elements } = data.Catalog.searchStore; - let free = elements.filter(offer => offer.promotions - && offer.promotions.promotionalOffers.length > 0 - && offer.promotions.promotionalOffers[0].promotionalOffers.find(p => p.discountSetting.discountPercentage === 0)); - let isBundle = promo => Boolean(promo.categories.find(cat => cat.path === "bundles")); - let getOffer = promo => (isBundle(promo) - ? client.getBundleForSlug(promo.productSlug.split('/')[0], locale) - : client.getProductForSlug(promo.productSlug.split('/')[0], locale)); - - let freeOffers = await Promise.all(free.map(async promo => { - let o = await getOffer(promo); - let page; - if (o.pages) { - page = o.pages.find(p => p._urlPattern.includes(promo.productSlug)); - } else { - page = o; + let free = elements.filter((offer) => offer.promotions + && offer.promotions.promotionalOffers.length > 0 + && offer.promotions.promotionalOffers[0].promotionalOffers.find((p) => p.discountSetting.discountPercentage === 0)); + + let isBundle = (promo) => Boolean(promo.categories.find((cat) => cat.path === "bundles")); + // eslint-disable-next-line no-confusing-arrow + let getOffer = (promo) => isBundle(promo) + ? client.getBundleForSlug(promo.productSlug.split("/")[0], locale) + : client.getProductForSlug(promo.productSlug.split("/")[0], locale); + + let freeOffers = await Promise.all(free.map(async(promo) => { + let offer = await getOffer(promo); + let page = offer; + + if (offer.pages) { + page = offer.pages.find((p) => p._urlPattern.includes(promo.productSlug)); } + if (!page) { - [page] = o.pages; + [page] = offer.pages; } return { - "title": o.productName || o._title, + "title": offer.productName || offer._title, "id": page.offer.id, - "namespace": page.offer.namespace - } + "namespace": page.offer.namespace, + }; })); return freeOffers; diff --git a/test/gamePromotions.spec.mjs b/test/gamePromotions.spec.mjs index 9bf0ed8..798bf79 100644 --- a/test/gamePromotions.spec.mjs +++ b/test/gamePromotions.spec.mjs @@ -1,67 +1,68 @@ +/* eslint-env mocha */ +/* globals expect */ +"use strict"; -import 'chai/register-expect.js'; -import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import "chai/register-expect.js"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import gamePromotions from "../src/gamePromotions.js"; +import { readFileSync } from "fs"; const __dirname = dirname(fileURLToPath(import.meta.url)); -import gamePromotions from '../src/gamePromotions.js'; - function readData(name, date = null) { - let filename = name.replace('/', '_'); + let filename = name.replace("/", "_"); if (date) { - filename += "_" + date; + filename += `_${date}`; } return JSON.parse(readFileSync(`${__dirname}/data/${filename}.json`).toString()); } let client = {}; -client.getBundleForSlug = async function (slug) { return readData(`bundles_${slug}`, this.date) }; -client.getProductForSlug = async function (slug) { return readData(`products_${slug}`, this.date) }; - -describe('freeGamesPromotions', () => { +// eslint-disable-next-line +client.getBundleForSlug = async function(slug) { return readData(`bundles_${slug}`, this.date); }; +// eslint-disable-next-line +client.getProductForSlug = async function(slug) { return readData(`products_${slug}`, this.date); }; - let target = async (date) => { +describe("freeGamesPromotions", () => { + let target = async(date) => { client.date = date; - client.freeGamesPromotions = () => readData('freeGamesPromotions', date); + client.freeGamesPromotions = () => readData("freeGamesPromotions", date); return (await gamePromotions.freeGamesPromotions(client)) - .map((o) => ({ title: o.title, id: o.id, namespace: o.namespace })); - } + .map((o) => ({ "title": o.title, "id": o.id, "namespace": o.namespace })); + }; let date = "2020-09-01"; context(`On ${date}`, () => { - - it('should return current 100% discounted games', async function () { - + it("should return current 100% discounted games", async() => { let freeGames = await target(date); - expect(freeGames).to.deep.include({title: 'Shadowrun Collection', - id: 'c5cb60ecc6554c6a8d9a682b87ab04bb', - namespace: 'bd8a7e894699493fb21503837f7b66c5'}); + expect(freeGames).to.deep.include({ + "title": "Shadowrun Collection", + "id": "c5cb60ecc6554c6a8d9a682b87ab04bb", + "namespace": "bd8a7e894699493fb21503837f7b66c5", + }); - expect(freeGames).to.deep.include({title: 'Hitman 2016', - id: 'e8efad3d47a14284867fef2c347c321d', - namespace: '3c06b15a8a2845c0b725d4f952fe00aa'}); + expect(freeGames).to.deep.include({ + "title": "Hitman 2016", + "id": "e8efad3d47a14284867fef2c347c321d", + "namespace": "3c06b15a8a2845c0b725d4f952fe00aa", + }); expect(freeGames).to.have.lengthOf(2); }); - it('should not return coming discounted games', async function () { - + it("should not return coming discounted games", async() => { let freeGames = await target(date); - expect(freeGames).to.not.deep.include({namespace: '5c0d568c71174cff8026db2606771d96'}); + expect(freeGames).to.not.deep.include({ "namespace": "5c0d568c71174cff8026db2606771d96" }); }); - it('should not return other free games', async function () { - + it("should not return other free games", async() => { let freeGames = await target(date); - expect(freeGames).to.not.deep.include({namespace: 'd6a3cae34c5d4562832610b5b8664576'}); + expect(freeGames).to.not.deep.include({ "namespace": "d6a3cae34c5d4562832610b5b8664576" }); }); - }); - });