Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: retry logic for nvidia session token and adding to cart #347

Merged
merged 1 commit into from
Sep 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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