diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000000..bb6fa4d536144c --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port \ No newline at end of file diff --git a/database/.gitkeep b/database/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/database/offers/.gitkeep b/database/offers/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000000..0249bb79291a86 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,88 @@ +{ + "name": "offers-tracker", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" + }, + "cross-fetch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", + "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", + "requires": { + "node-fetch": "2.1.2", + "whatwg-fetch": "2.0.4" + } + }, + "graphql-request": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", + "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", + "requires": { + "cross-fetch": "2.2.2" + } + }, + "node-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, + "simple-git": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.11.0.tgz", + "integrity": "sha512-wFePCEQYY6BzVOg/BuUVEhr3jZPF/cPG/BN2UXgax6NHc3bJ9UrDc5AME281gs2C7J1UZ6BGRJYT64khx9T+ng==", + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.0.1", + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000000..8de4a0fdf3c7ef --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "offers-tracker", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "update": "node update" + }, + "author": { + "name": "Szymon Lisowiec", + "url": "https://kysune.me" + }, + "dependencies": { + "graphql-request": "1.8.2", + "simple-git": "^2.11.0" + } +} diff --git a/queries/FetchStoreOffersByNamespaceQuery.graphql b/queries/FetchStoreOffersByNamespaceQuery.graphql new file mode 100644 index 00000000000000..fba63f9c9b21ad --- /dev/null +++ b/queries/FetchStoreOffersByNamespaceQuery.graphql @@ -0,0 +1,62 @@ +query catalogQuery($namespace:String!, $locale:String, $count:Int, $start:Int, $country: String!) { + Catalog { + catalogOffers( + namespace: $namespace, + locale: $locale, + params: {count: $count, start: $start, country: $country}, + countryAgeFilter: {shouldCheck: false} + ) { + elements { + isFeatured + collectionOfferIds + effectiveDate + creationDate + lastModifiedDate + viewableDate + productSlug + developer + ignoreOrder + freeDays + + title + id + namespace + description + longDescription + status + keyImages { + type + url + } + seller { + id + name + } + items { + id + namespace + developer + } + customAttributes { + key + value + } + categories { + path + } + linkedOfferId + linkedOffer { + customAttributes { + key + value + } + } + } + paging { + count + start + total + } + } + } +} \ No newline at end of file diff --git a/queries/FetchStoreOffersQuery.graphql b/queries/FetchStoreOffersQuery.graphql new file mode 100644 index 00000000000000..e87749067a5079 --- /dev/null +++ b/queries/FetchStoreOffersQuery.graphql @@ -0,0 +1,64 @@ +query catalogQuery($locale:String, $count:Int, $start:Int, $country: String!, $sortBy: String, $sortDir: String) { + Catalog { + searchStore ( + locale: $locale, + count: $count + start: $start + country: $country + sortBy: $sortBy + sortDir: $sortDir + ) { + elements { + isFeatured + collectionOfferIds + effectiveDate + creationDate + lastModifiedDate + viewableDate + productSlug + developer + ignoreOrder + freeDays + + title + id + namespace + description + longDescription + status + keyImages { + type + url + } + seller { + id + name + } + items { + id + namespace + developer + } + customAttributes { + key + value + } + categories { + path + } + linkedOfferId + linkedOffer { + customAttributes { + key + value + } + } + } + paging { + count + start + total + } + } + } +} \ No newline at end of file diff --git a/update.js b/update.js new file mode 100644 index 00000000000000..eca11dc2d00449 --- /dev/null +++ b/update.js @@ -0,0 +1,97 @@ +const Fs = require('fs'); +const SimpleGit = require('simple-git'); +const { GraphQLClient } = require('graphql-request'); + +const FetchStoreOffersQuery = Fs.readFileSync('./queries/FetchStoreOffersQuery.graphql', 'utf8'); +const FetchStoreOffersByNamespaceQuery = Fs.readFileSync('./queries/FetchStoreOffersByNamespaceQuery.graphql', 'utf8'); + +class Main { + constructor () { + this.language = 'en'; + this.country = 'US'; + this.namespaces = []; // You can add here non-store namespaces e.g. ue (unreal engine market offers) + + this.ql = new GraphQLClient('https://graphql.epicgames.com/graphql', { + headers: { + Origin: 'https://epicgames.com', + }, + }); + + this.update(); + } + + async update () { + console.log('Updating epicgames store offers...'); + await this.fetchAllOffers(FetchStoreOffersQuery, { + country: this.country, + locale: this.language, + sortBy: 'lastModifiedDate', + sortDir: 'DESC', + }, (result) => { + return result && result.Catalog && result.Catalog.searchStore || {}; + }); + + for (let i = 0; i < this.namespaces.length; ++i) { + const namespace = this.namespaces[i]; + console.log(`Updating offers for namespace ${namespace}...`); + await this.fetchAllOffers(FetchStoreOffersByNamespaceQuery, { + namespace, + country: this.country, + locale: this.language, + }, (result) => { + return result && result.Catalog && result.Catalog.catalogOffers || {}; + }); + } + + this.sync(); + } + + async sync () { + const git = SimpleGit({ + baseDir: __dirname, + binary: 'git', + }); + const add = git.add([`${__dirname}/database/.`]); + console.dir(add); + } + + saveOffer (offer) { + Fs.writeFile(`${__dirname}/database/offers/${offer.id}.json`, JSON.stringify(offer, null, 2), (error) => { + // console.log(`${offer.id} = ${!error ? 'OK' : error}`); + }); + } + + async fetchAllOffers (query, params, resultSelector) { + const elements = []; + let paging = {}; + do { + const result = await this.fetchOffers(query, params, resultSelector, paging.start, paging.count); + paging = result.paging; + paging.start += paging.count; + for (let i = 0; i < result.elements.length; ++i) { + const element = result.elements[i]; + this.saveOffer(element); + } + } while (paging.start - 1000 < paging.total - paging.count); + } + + async fetchOffers (query, params, resultSelector, start = 0, count = 1000) { + try { + let result = await this.ql.request(query, { + ...params, + start, + count, + }); + result = resultSelector(result); + return result; + } catch (err) { + console.dir(err); + if(!err.response.data) { + console.dir(err); + if (err.response && err.response.errors) console.log(JSON.stringify(err.response.errors, null, 2)); + }else data = err.response.data; + } + } +} + +module.exports = new Main();