Skip to content

Commit

Permalink
feat: retry logic for nvidia session token and adding to cart (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewmackrodt committed Sep 27, 2020
1 parent 3bde805 commit 1bac1b9
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 110 deletions.
2 changes: 2 additions & 0 deletions .env-example
Expand Up @@ -13,6 +13,8 @@ IN_STOCK_WAIT_TIME=""
LOG_LEVEL=""
LOW_BANDWIDTH=""
MICROCENTER_LOCATION=""
NVIDIA_ADD_TO_CART_ATTEMPTS=""
NVIDIA_SESSION_TTL=""
OPEN_BROWSER=""
PAGE_TIMEOUT=""
PHONE_NUMBER=""
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -80,6 +80,8 @@ Here is a list of variables that you can use to customize your newly copied `.en
| `LOG_LEVEL` | [Logging levels](https://github.com/winstonjs/winston#logging-levels) | Debugging related, default: `info` |
| `LOW_BANDWIDTH` | Blocks images/fonts to reduce traffic | Disables ad blocker, default: `false` |
| `MICROCENTER_LOCATION` | Specific MicroCenter location to search | Default : `web` |
| `NVIDIA_ADD_TO_CART_ATTEMPTS` | The maximum number of times the `nvidia-api` add to cart feature will be attempted before failing | Default: `10` |
| `NVIDIA_SESSION_TTL` | The time in seconds to keep the cart active while using `nvidia-api` | Default: `60000` |
| `OPEN_BROWSER` | Toggle for whether or not the browser should open when item is found | Default: `true` |
| `PAGE_TIMEOUT` | Navigation Timeout in milliseconds | `0` for infinite, default: `30000` |
| `PHONE_NUMBER` | 10 digit phone number | E.g.: `1234567890`, email configuration required |
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Expand Up @@ -113,6 +113,11 @@ const notifications = {
}
};

const nvidia = {
addToCardAttempts: envOrNumber(process.env.NVIDIA_ADD_TO_CART_ATTEMPTS, 10),
sessionTtl: envOrNumber(process.env.NVIDIA_SESSION_TTL, 60000)
};

const page = {
height: 1080,
inStockWaitTime: envOrNumber(process.env.IN_STOCK_WAIT_TIME),
Expand All @@ -135,6 +140,7 @@ export const Config = {
browser,
logLevel,
notifications,
nvidia,
page,
store
};
174 changes: 174 additions & 0 deletions src/store/model/helpers/nvidia-cart.ts
@@ -0,0 +1,174 @@
import {NvidiaRegionInfo, regionInfos} from '../nvidia-api';
import {usingPage, usingResponse} from '../../../util';
import {Browser} from 'puppeteer';
import {Config} from '../../../config';
import {Logger} from '../../../logger';
import open from 'open';

interface NvidiaSessionTokenJSON {
session_token: string;
}

interface NvidiaAddToCardJSON {
location: string;
}

export class NvidiaCart {
protected readonly browser: Browser;
protected isKeepAlive = false;
protected sessionToken: string | null = null;

public constructor(browser: Browser) {
this.browser = browser;
}

public keepAlive() {
if (this.isKeepAlive) {
return;
}

const callback = async () => {
if (!this.isKeepAlive) {
return;
}

await this.refreshSessionToken();

setTimeout(callback, Config.nvidia.sessionTtl);
};

this.isKeepAlive = true;

void callback();
}

public get fallbackCartUrl(): string {
return `https://www.nvidia.com/${this.regionInfo.siteLocale}/geforce/`;
}

public get regionInfo(): NvidiaRegionInfo {
const country = Config.store.country;
const regionInfo = regionInfos.get(country);
if (!regionInfo) {
throw new Error(`Unknown country ${country}`);
}

return regionInfo;
}

public get sessionUrl(): string {
return `https://store.nvidia.com/store/nvidia/SessionToken?format=json&locale=${this.regionInfo.drLocale}`;
}

public async addToCard(productId: number, name: string): Promise<string> {
let cartUrl: string | undefined;
Logger.info(`🚀🚀🚀 [nvidia] ${name}, starting auto add to cart 🚀🚀🚀`);
try {
Logger.info(`🚀🚀🚀 [nvidia] ${name}, adding to cart 🚀🚀🚀`);
let lastError: Error | string | undefined;

/* eslint-disable no-await-in-loop */
for (let i = 0; i < Config.nvidia.addToCardAttempts; i++) {
try {
cartUrl = await this.addToCartAndGetLocationRedirect(productId);

break;
} catch (error) {
Logger.error(`✖ [nvidia] ${name} could not automatically add to cart, attempt ${i + 1} of ${Config.nvidia.addToCardAttempts}`, error);
Logger.debug(error);

lastError = error;
}
}
/* eslint-enable no-await-in-loop */

if (!cartUrl) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw lastError;
}

Logger.info(`🚀🚀🚀 [nvidia] ${name}, opening checkout page 🚀🚀🚀`);
Logger.info(cartUrl);

await open(cartUrl);
} catch (error) {
Logger.error(`✖ [nvidia] ${name} could not automatically add to cart, opening page`);
Logger.debug(error);

cartUrl = this.fallbackCartUrl;

await open(cartUrl);
}

return cartUrl;
}

public async getSessionToken(): Promise<string> {
if (!this.sessionToken) {
await this.refreshSessionToken();
}

if (!this.sessionToken) {
throw new Error('Failed to create the session_token');
}

return this.sessionToken;
}

public async refreshSessionToken(): Promise<void> {
Logger.debug('ℹ [nvidia] refreshing session token');
try {
const result = await usingResponse(this.browser, this.sessionUrl, async response => {
return response?.json() as NvidiaSessionTokenJSON | undefined;
});
if (typeof result !== 'object' || result === null || !('session_token' in result)) {
throw new Error('malformed response');
}

this.sessionToken = result.session_token;
Logger.debug(`ℹ [nvidia] session_token=${result.session_token}`);
} catch (error) {
const message: string = typeof error === 'object' ? error.message : error;
Logger.error(`✖ [nvidia] ${message}`);
}
}

protected async addToCartAndGetLocationRedirect(productId: number): Promise<string> {
const url = 'https://api-prod.nvidia.com/direct-sales-shop/DR/add-to-cart';
const sessionToken = await this.getSessionToken();

Logger.info(`ℹ [nvidia] session_token=${sessionToken}`);

const locationData = await usingPage(this.browser, async page => {
page.removeAllListeners('request');

await page.setRequestInterception(true);

page.on('request', interceptedRequest => {
void interceptedRequest.continue({
headers: {
...interceptedRequest.headers(),
'content-type': 'application/json',
nvidia_shop_id: sessionToken
},
method: 'POST',
postData: JSON.stringify({
products: [
{productId, quantity: 1}
]
})
});
});

const response = await page.goto(url, {waitUntil: 'networkidle0'});

if (response === null) {
throw new Error('NvidiaAddToCartUnavailable');
}

return response.json() as Promise<NvidiaAddToCardJSON>;
});

return locationData.location;
}
}
104 changes: 16 additions & 88 deletions src/store/model/helpers/nvidia.ts
@@ -1,9 +1,8 @@
import {Browser, Page, Response} from 'puppeteer';
import {NvidiaRegionInfo, regionInfos} from '../nvidia-api';
import {Browser} from 'puppeteer';
import {Config} from '../../../config';
import {Link} from '../store';
import {Logger} from '../../../logger';
import open from 'open';
import {NvidiaCart} from './nvidia-cart';
import {timestampUrlParameter} from '../../timestamp-url-parameter';

function getRegionInfo(): NvidiaRegionInfo {
Expand All @@ -25,94 +24,23 @@ function nvidiaStockUrl(id: number, drLocale: string, currency: string): string
timestampUrlParameter().slice(1);
}

interface NvidiaSessionTokenJSON {
session_token: string;
}

interface NvidiaAddToCardJSON {
location: string;
}

function nvidiaSessionUrl(drLocale: string): string {
return `https://store.nvidia.com/store/nvidia/SessionToken?format=json&locale=${drLocale}` +
timestampUrlParameter();
}

async function addToCartAndGetLocationRedirect(page: Page, sessionToken: string, productId: number): Promise<string> {
const url = 'https://api-prod.nvidia.com/direct-sales-shop/DR/add-to-cart';

page.removeAllListeners('request');
let cart: NvidiaCart;

await page.setRequestInterception(true);

page.on('request', interceptedRequest => {
void interceptedRequest.continue({
headers: {
...interceptedRequest.headers(),
'content-type': 'application/json',
nvidia_shop_id: sessionToken
},
method: 'POST',
postData: JSON.stringify({
products: [
{productId, quantity: 1}
]
})
});
});

const response = await page.goto(url, {waitUntil: 'networkidle0'});
if (response === null) {
throw new Error('NvidiaAddToCartUnavailable');
}

const locationData = await response.json() as NvidiaAddToCardJSON;

return locationData.location;
}

function fallbackCartUrl(nvidiaLocale: string): string {
return `https://www.nvidia.com/${nvidiaLocale}/shop/geforce?${timestampUrlParameter()}`;
}

export function generateOpenCartAction(id: number, drLocale: string, cardName: string) {
export function generateSetupAction() {
return async (browser: Browser) => {
const page = await browser.newPage();

Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, starting auto add to cart 🚀🚀🚀`);

let response: Response | null;
let cartUrl: string;
try {
Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, getting access token 🚀🚀🚀`);

response = await page.goto(nvidiaSessionUrl(drLocale), {waitUntil: 'networkidle0'});
if (response === null) {
throw new Error('NvidiaAccessTokenUnavailable');
}

const data = await response.json() as NvidiaSessionTokenJSON;
const sessionToken = data.session_token;
cart = new NvidiaCart(browser);

Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, adding to cart 🚀🚀🚀`);

cartUrl = await addToCartAndGetLocationRedirect(page, sessionToken, id);

Logger.info(`🚀🚀🚀 [nvidia] ${cardName}, opening checkout page 🚀🚀🚀`);
Logger.info(cartUrl);

await open(cartUrl);
} catch (error) {
Logger.debug(error);
Logger.error(`✖ [nvidia] ${cardName} could not automatically add to cart, opening page`, error);

cartUrl = fallbackCartUrl(drLocale);
await open(cartUrl);
if (Config.browser.open) {
cart.keepAlive();
}
};
}

await page.close();
export function generateOpenCartAction(id: number, cardName: string) {
return async () => {
const url = await cart.addToCard(id, cardName);

return cartUrl;
return url;
};
}

Expand All @@ -125,7 +53,7 @@ export function generateLinks(): Link[] {
links.push({
brand: 'test:brand',
model: 'test:model',
openCartAction: generateOpenCartAction(fe2060SuperId, drLocale, 'TEST CARD debug'),
openCartAction: generateOpenCartAction(fe2060SuperId, 'TEST CARD debug'),
series: 'test:series',
url: nvidiaStockUrl(fe2060SuperId, drLocale, currency)
});
Expand All @@ -135,7 +63,7 @@ export function generateLinks(): Link[] {
links.push({
brand: 'nvidia',
model: 'founders edition',
openCartAction: generateOpenCartAction(fe3080Id, drLocale, 'nvidia founders edition 3080'),
openCartAction: generateOpenCartAction(fe3080Id, 'nvidia founders edition 3080'),
series: '3080',
url: nvidiaStockUrl(fe3080Id, drLocale, currency)
});
Expand All @@ -145,7 +73,7 @@ export function generateLinks(): Link[] {
links.push({
brand: 'nvidia',
model: 'founders edition',
openCartAction: generateOpenCartAction(fe3090Id, drLocale, 'nvidia founders edition 3090'),
openCartAction: generateOpenCartAction(fe3090Id, 'nvidia founders edition 3090'),
series: '3090',
url: nvidiaStockUrl(fe3090Id, drLocale, currency)
});
Expand Down

0 comments on commit 1bac1b9

Please sign in to comment.