diff --git a/.env-example b/.env-example index 700c09c087..3e47e50a1a 100644 --- a/.env-example +++ b/.env-example @@ -19,6 +19,7 @@ SHOW_ONLY_SERIES="3080" SLACK_CHANNEL="SlackChannelName" SLACK_TOKEN="slack-token" STORES="bestbuy,bandh,nvidia" +COUNTRY="usa" SCREENSHOT="true" TELEGRAM_ACCESS_TOKEN="" TELEGRAM_CHAT_ID="1234" diff --git a/README.md b/README.md index a5d4c7a680..45fe673a11 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ Here is a list of variables that you can use to customize your newly copied `.en | `SLACK_CHANNEL` | Slack channel for posting | E.g., `update`, no need for `#` | | `SLACK_TOKEN` | Slack API token | | `STORES` | [Supported stores](#supported-stores) you want to be scraped | Comma separated, default: `nvidia` | +| `COUNTRY` | [Supported country](#supported-countries) you want to be scraped, currently only used by Nvidia | default: `usa` | +| `SCREENSHOT` | Capture screenshot of page if a card is found | Default: `true` | | `TELEGRAM_ACCESS_TOKEN` | Telegram access token | | `TELEGRAM_CHAT_ID` | Telegram chat ID | | `USER_AGENT` | Custom User-Agent header for HTTP requests | Default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36` | @@ -124,6 +126,29 @@ Here is a list of variables that you can use to customize your newly copied `.en | T-Mobile | `tmobile`| | | Verizon | `verizon`| Works with Visible | +#### Supported countries + +| **Country** | **Nvidia.com (3080 FE)** | **Nvidia.com (3090 FE)** | **Notes** | +|:---:|:---:|:---:|:---:| +| austria | `✔` | | | +| belgium | `✔` | | Nvidia supports debug | +| canada | `✔` | | | +| czechia | `✔` | | | +| denmark | `✔` | | | +| finland | `✔` | | | +| france | `✔` | | | +| germany | `✔` | | | +| great_britain | `✔` | | | +| ireland | `✔` | | | +| italy | `✔` | | | +| luxembourg | `✔` | | Nvidia supports debug | +| poland | `✔` | | | +| portugal | `✔` | | | +| russia | | | Missing all IDs | +| spain | `✔` | | | +| sweden | `✔` | | | +| usa | `✔` | | Nvidia supports debug | + ## FAQ **Q: What's Node.js and how do I install it?** Visit [their website](https://nodejs.org/en/) and download and install it. Very straight forward. Otherwise, Google more information related to your system needs. diff --git a/src/config.ts b/src/config.ts index 5e61f72b2c..0125fb4a42 100644 --- a/src/config.ts +++ b/src/config.ts @@ -63,7 +63,8 @@ const page = { const store = { showOnlySeries: process.env.SHOW_ONLY_SERIES ? process.env.SHOW_ONLY_SERIES.split(',') : ['3070', '3080', '3090'], showOnlyBrands: process.env.SHOW_ONLY_BRANDS ? process.env.SHOW_ONLY_BRANDS.split(',') : [], - stores: process.env.STORES ? process.env.STORES.split(',') : ['nvidia'] + stores: process.env.STORES ? process.env.STORES.split(',') : ['nvidia'], + country: process.env.COUNTRY ?? 'usa' }; export const Config = { diff --git a/src/index.ts b/src/index.ts index 68c7b00198..81a981cb17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,10 @@ async function main() { for (const store of Stores) { Logger.debug(store.links); + if (store.setupAction !== undefined) { + store.setupAction(browser); + } + setTimeout(tryLookupAndLoop, getSleepTime(), browser, store); } } diff --git a/src/store/lookup.ts b/src/store/lookup.ts index dfe7c91c3c..986a7211fe 100644 --- a/src/store/lookup.ts +++ b/src/store/lookup.ts @@ -57,7 +57,7 @@ async function lookup(browser: Browser, store: Store) { page.setDefaultNavigationTimeout(Config.page.navigationTimeout); await page.setUserAgent(Config.page.userAgent); - const graphicsCard = `${link.brand} ${link.model}`; + const graphicsCard = `${link.brand} ${link.model} ${link.series}`; let response: Response | null; try { diff --git a/src/store/model/helpers/nvidia.ts b/src/store/model/helpers/nvidia.ts new file mode 100644 index 0000000000..f8c5b25fbc --- /dev/null +++ b/src/store/model/helpers/nvidia.ts @@ -0,0 +1,148 @@ +import {timestampUrlParameter} from '../../timestamp-url-parameter'; +import {Browser, Response} from 'puppeteer'; +import {Logger} from '../../../logger'; +import open from 'open'; +import {Link} from '../store'; +import {Config} from '../../../config'; +import {NvidiaRegionInfo, regionInfos} from '../nvidia'; + +const nvidiaApiKey = '9485fa7b159e42edb08a83bde0d83dia'; + +function getRegionInfo(): NvidiaRegionInfo { + const country = Array.from(regionInfos.keys()).includes(Config.store.country) ? Config.store.country : 'usa'; + + const defaultRegionInfo: NvidiaRegionInfo = {drLocale: 'en_us', nvidiaLocale: 'en_us', fe3080Id: 5438481700, fe3090Id: null, fe2060SuperId: 5379432500}; + return regionInfos.get(country) ?? defaultRegionInfo; +} + +function digitalRiverStockUrl(id: number, drLocale: string): string { + return `https://api.digitalriver.com/v1/shoppers/me/products/${id}/inventory-status?` + + `&apiKey=${nvidiaApiKey}` + + `&locale=${drLocale}` + + timestampUrlParameter(); +} + +interface NvidiaSessionTokenJSON { + access_token: string; +} + +function nvidiaSessionUrl(nvidiaLocale: string): string { + return `https://store.nvidia.com/store/nvidia/SessionToken?format=json&locale=${nvidiaLocale}` + + `&apiKey=${nvidiaApiKey}` + + timestampUrlParameter(); +} + +function addToCartUrl(id: number, drLocale: string, token: string): string { + return 'https://api.digitalriver.com/v1/shoppers/me/carts/active/line-items?format=json&method=post' + + `&productId=${id}` + + `&token=${token}` + + '&quantity=1' + + `&locale=${drLocale}` + + timestampUrlParameter(); +} + +function checkoutUrl(drLocale: string, token: string): string { + return `https://api.digitalriver.com/v1/shoppers/me/carts/active/web-checkout?token=${token}&locale=${drLocale}`; +} + +function fallbackCartUrl(nvidiaLocale: string): string { + return `https://www.nvidia.com/${nvidiaLocale}/shop/geforce?${timestampUrlParameter()}`; +} + +export function generateSetupAction() { + return async (browser: Browser) => { + const {drLocale, nvidiaLocale} = getRegionInfo(); + + const page = await browser.newPage(); + + Logger.info('[nvidia] creating cart/session token...'); + let response: Response | null; + try { + response = await page.goto(nvidiaSessionUrl(nvidiaLocale), {waitUntil: 'networkidle0'}); + if (response === null) { + throw new Error('NvidiaAccessTokenUnavailable'); + } + + const data = await response.json() as NvidiaSessionTokenJSON; + const accessToken = data.access_token; + + Logger.info('[nvidia] you can log into your cart now...'); + Logger.info(checkoutUrl(drLocale, accessToken)); + await open(checkoutUrl(drLocale, accessToken)); + } catch (error) { + Logger.debug(error); + Logger.error('✖ [nvidia] cannot generate cart/session token, continuing without, auto-"add to cart" may not work...'); + } + + await page.close(); + }; +} + +export function generateOpenCartAction(id: number, nvidiaLocale: string, drLocale: string, cardName: string) { + return async (browser: Browser) => { + const page = await browser.newPage(); + Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, starting auto add to cart... 🚀🚀🚀`); + let response: Response | null; + try { + Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, getting access token... 🚀🚀🚀`); + response = await page.goto(nvidiaSessionUrl(nvidiaLocale), {waitUntil: 'networkidle0'}); + if (response === null) { + throw new Error('NvidiaAccessTokenUnavailable'); + } + + const data = await response.json() as NvidiaSessionTokenJSON; + const accessToken = data.access_token; + + Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, adding to cart... 🚀🚀🚀`); + response = await page.goto(addToCartUrl(id, drLocale, accessToken), {waitUntil: 'networkidle0'}); + + Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, opening checkout page... 🚀🚀🚀`); + Logger.info(checkoutUrl(drLocale, accessToken)); + await open(checkoutUrl(drLocale, accessToken)); + } catch (error) { + Logger.debug(error); + Logger.error(`✖ [nvidia] ${cardName} could not automatically add to cart, opening page`); + await open(fallbackCartUrl(nvidiaLocale)); + } + + await page.close(); + }; +} + +export function generateLinks(): Link[] { + const {drLocale, nvidiaLocale, fe3080Id, fe3090Id, fe2060SuperId} = getRegionInfo(); + + const links: Link[] = []; + + if (fe2060SuperId) { + links.push({ + series: 'debug', + brand: 'TEST', + model: 'CARD', + url: digitalRiverStockUrl(fe2060SuperId, drLocale), + openCartAction: generateOpenCartAction(fe2060SuperId, nvidiaLocale, drLocale, 'TEST CARD debug') + }); + } + + if (fe3080Id) { + links.push({ + series: '3080', + brand: 'nvidia', + model: 'founders edition', + url: digitalRiverStockUrl(fe3080Id, drLocale), + openCartAction: generateOpenCartAction(fe3080Id, nvidiaLocale, drLocale, 'nvidia founders edition 3080') + }); + } + + if (fe3090Id) { + links.push({ + series: '3090', + brand: 'nvidia', + model: 'founders edition', + url: digitalRiverStockUrl(fe3090Id, drLocale), + openCartAction: generateOpenCartAction(fe3090Id, nvidiaLocale, drLocale, 'nvidia founders edition 3090') + }); + } + + return links; +} diff --git a/src/store/model/nvidia.ts b/src/store/model/nvidia.ts index 82d568340f..66edf73edb 100644 --- a/src/store/model/nvidia.ts +++ b/src/store/model/nvidia.ts @@ -1,96 +1,42 @@ import {Store} from './store'; -import {Browser, Response} from 'puppeteer'; -import {timestampUrlParameter} from '../timestamp-url-parameter'; -import {Logger} from '../../logger'; -import open from 'open'; +import {generateLinks, generateSetupAction} from './helpers/nvidia'; -const fe2060SuperId = 5379432500; -const fe3080Id = 5438481700; -const locale = 'en_us'; +// Region/country set by config file, silently ignores null / missing values and defaults to usa -const nvidiaApiKey = '9485fa7b159e42edb08a83bde0d83dia'; - -function digitalRiverStockUrl(id: number): string { - return `https://api.digitalriver.com/v1/shoppers/me/products/${id}/inventory-status?` + - `&apiKey=${nvidiaApiKey}` + - timestampUrlParameter(); -} - -interface NvidiaSessionTokenJSON { - access_token: string; -} - -function nvidiaSessionUrl(): string { - return `https://store.nvidia.com/store/nvidia/SessionToken?format=json&locale=${locale}` + - `&apiKey=${nvidiaApiKey}` + - timestampUrlParameter(); -} - -function addToCartUrl(id: number, token: string): string { - return 'https://api.digitalriver.com/v1/shoppers/me/carts/active/line-items?format=json&method=post' + - `&productId=${id}` + - `&token=${token}` + - '&quantity=1' + - timestampUrlParameter(); -} - -function checkoutUrl(token: string): string { - return `https://api.digitalriver.com/v1/shoppers/me/carts/active/web-checkout?token=${token}`; -} - -function fallbackCartUrl(): string { - return `https://www.nvidia.com/en-us/shop/geforce?${timestampUrlParameter()}`; +export interface NvidiaRegionInfo { + drLocale: string; + nvidiaLocale: string; + fe3080Id: number | null; + fe3090Id: number | null; + fe2060SuperId: number | null; } -function generateCartAction(id: number, cardName: string) { - return async (browser: Browser) => { - const page = await browser.newPage(); - Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, starting auto add to cart... 🚀🚀🚀`); - let response: Response | null; - try { - Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, getting access token... 🚀🚀🚀`); - response = await page.goto(nvidiaSessionUrl(), {waitUntil: 'networkidle0'}); - if (response === null) { - throw new Error('NvidiaAccessTokenUnavailable'); - } - - const data = await response.json() as NvidiaSessionTokenJSON; - const accessToken = data.access_token; - - Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, adding to cart... 🚀🚀🚀`); - response = await page.goto(addToCartUrl(id, accessToken), {waitUntil: 'networkidle0'}); - - Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, opening checkout page... 🚀🚀🚀`); - Logger.info(checkoutUrl(accessToken)); - await open(checkoutUrl(accessToken)); - } catch { - Logger.error(`✖ [nvidia] ${cardName} could not automatically add to cart, opening page`); - await open(fallbackCartUrl()); - } - - await page.close(); - }; -} +export const regionInfos = new Map([ + ['austria', {drLocale: 'de_de', nvidiaLocale: 'de_de', fe3080Id: 5440853700, fe3090Id: null, fe2060SuperId: null}], + ['belgium', {drLocale: 'fr_fr', nvidiaLocale: 'fr_fr', fe3080Id: 5438795700, fe3090Id: null, fe2060SuperId: 5394902700}], + ['canada', {drLocale: 'en_us', nvidiaLocale: 'en_ca', fe3080Id: 5438481700, fe3090Id: null, fe2060SuperId: null}], + ['czechia', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438793800, fe3090Id: null, fe2060SuperId: null}], + ['denmark', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438793300, fe3090Id: null, fe2060SuperId: null}], + ['finland', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438793300, fe3090Id: null, fe2060SuperId: null}], + ['france', {drLocale: 'fr_fr', nvidiaLocale: 'fr_fr', fe3080Id: 5438795200, fe3090Id: null, fe2060SuperId: null}], + ['germany', {drLocale: 'de_de', nvidiaLocale: 'de_de', fe3080Id: 5438792300, fe3090Id: null, fe2060SuperId: null}], + ['great_britain', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438792800, fe3090Id: null, fe2060SuperId: null}], + ['ireland', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438792800, fe3090Id: null, fe2060SuperId: null}], + ['italy', {drLocale: 'it_it', nvidiaLocale: 'it_it', fe3080Id: 5438796200, fe3090Id: null, fe2060SuperId: null}], + ['luxembourg', {drLocale: 'fr_fr', nvidiaLocale: 'fr_fr', fe3080Id: 5438795700, fe3090Id: null, fe2060SuperId: 5394902700}], + ['poland', {drLocale: 'pl_pl', nvidiaLocale: 'pl_pl', fe3080Id: 5438797700, fe3090Id: null, fe2060SuperId: null}], + ['portugal', {drLocale: 'en_gb', nvidiaLocale: 'en_gb', fe3080Id: 5438794300, fe3090Id: null, fe2060SuperId: null}], + ['russia', {drLocale: 'ru_ru', nvidiaLocale: 'ru_ru', fe3080Id: null, fe3090Id: null, fe2060SuperId: null}], + ['spain', {drLocale: 'es_es', nvidiaLocale: 'es_es', fe3080Id: 5438794800, fe3090Id: null, fe2060SuperId: null}], + ['sweden', {drLocale: 'sv_SE', nvidiaLocale: 'sv_se', fe3080Id: 5438798100, fe3090Id: null, fe2060SuperId: null}], + ['usa', {drLocale: 'en_us', nvidiaLocale: 'en_us', fe3080Id: 5438481700, fe3090Id: null, fe2060SuperId: 5379432500}] +]); export const Nvidia: Store = { - links: [ - { - series: 'debug', - brand: 'TEST', - model: 'CARD', - url: digitalRiverStockUrl(fe2060SuperId), - openCartAction: generateCartAction(fe2060SuperId, 'TEST CARD') - }, - { - series: '3080', - brand: 'nvidia', - model: 'founders edition', - url: digitalRiverStockUrl(fe3080Id), - openCartAction: generateCartAction(fe3080Id, 'nvidia founders edition 3080') - } - ], + links: generateLinks(), labels: { outOfStock: ['product_inventory_out_of_stock', 'rate limit exceeded', 'request timeout'] }, - name: 'nvidia' + name: 'nvidia', + setupAction: generateSetupAction() }; diff --git a/src/store/model/store.ts b/src/store/model/store.ts index 5c2481514f..d3fec778ea 100644 --- a/src/store/model/store.ts +++ b/src/store/model/store.ts @@ -20,4 +20,5 @@ export interface Store { links: Link[]; labels: Labels; name: string; + setupAction?: (browser: Browser) => void; }