refactor: lookup, rm defaults (#69)

prep work for #38

Signed-off-by: Jef LeCompte <jeffreylec@gmail.com>
This commit is contained in:
Jef LeCompte
2020-09-19 12:45:03 -04:00
committed by GitHub
parent 3b2ba29cf1
commit ea5b7a0918
19 changed files with 142 additions and 134 deletions
+3 -74
View File
@@ -1,9 +1,8 @@
import {Config} from './config'; import {Config} from './config';
import {Store, Stores} from './store'; import {Stores} from './store/model';
import puppeteer from 'puppeteer';
import open from 'open';
import sendNotification from './notification';
import {Logger} from './logger'; import {Logger} from './logger';
import {sendNotification} from './notification';
import {lookup} from './store';
/** /**
* Starts the bot. * Starts the bot.
@@ -21,76 +20,6 @@ async function main() {
setTimeout(main, Config.rateLimitTimeout); setTimeout(main, Config.rateLimitTimeout);
} }
/**
* Responsible for looking up information about a each product within
* a `Store`. It's important that we ignore `no-await-in-loop` here
* because we don't want to get rate limited within the same store.
*
* @param store Vendor of graphics cards.
*/
async function lookup(store: Store) {
/* eslint-disable no-await-in-loop */
for (const link of store.links) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.setDefaultNavigationTimeout(Config.page.navigationTimeout);
await page.setUserAgent(Config.page.userAgent);
await page.setViewport({
height: Config.page.height,
width: Config.page.width
});
const graphicsCard = `${link.brand} ${link.model}`;
try {
await page.goto(link.url, {waitUntil: 'networkidle0'});
} catch {
Logger.error(`✖ [${store.name}] ${graphicsCard} skipping; timed out`);
await browser.close();
return;
}
const bodyHandle = await page.$('body');
const textContent = await page.evaluate(body => body.textContent, bodyHandle);
Logger.debug(textContent);
if (isOutOfStock(textContent, link.oosLabels)) {
Logger.info(`✖ [${store.name}] ${graphicsCard} is still out of stock`);
} else {
Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`);
Logger.info(link.url);
if (Config.page.capture === 'true') {
Logger.debug(' saving screenshot');
await page.screenshot({path: `success-${Date.now()}.png`});
}
const givenUrl = store.cartUrl ? store.cartUrl : link.url;
if (Config.openBrowser === 'true') {
await open(givenUrl);
}
sendNotification(givenUrl);
}
await browser.close();
}
/* eslint-enable no-await-in-loop */
}
/**
* Checks if DOM has any out-of-stock related text.
*
* @param domText Complete DOM of website.
* @param oosLabels Out-of-stock labels.
*/
function isOutOfStock(domText: string, oosLabels: string[]) {
const domTextLowerCase = domText.toLowerCase();
return oosLabels.some(label => domTextLowerCase.includes(label));
}
/** /**
* Send test email. * Send test email.
*/ */
+1 -1
View File
@@ -20,7 +20,7 @@ const mailOptions: Mail.Options = {
subject subject
}; };
export default function sendEmail(text: string) { export function sendEmail(text: string) {
mailOptions.text = text; mailOptions.text = text;
transporter.sendMail(mailOptions, (error, info) => { transporter.sendMail(mailOptions, (error, info) => {
+1 -26
View File
@@ -1,26 +1 @@
import {Config} from '../config'; export * from './notification';
import sendEmail from './email';
import sendSlaskMessage from './slack';
import sendSMS from './sms';
import playSound from './sound';
export default function sendNotification(cartUrl: string) {
if (Config.notifications.email.username && Config.notifications.email.password) {
sendEmail(cartUrl);
}
if (Config.notifications.slack.channel && Config.notifications.slack.token) {
sendSlaskMessage(cartUrl);
}
if (Config.notifications.phone.number) {
const carrier = Config.notifications.phone.carrier.toLowerCase();
if (carrier && Config.notifications.phone.availableCarriers.has(carrier)) {
sendSMS(cartUrl);
}
}
if (Config.notifications.playSound) {
playSound();
}
}
+26
View File
@@ -0,0 +1,26 @@
import {Config} from '../config';
import {sendEmail} from './email';
import {sendSMS} from './sms';
import {playSound} from './sound';
import {sendSlackMessage} from './slack';
export function sendNotification(cartUrl: string) {
if (Config.notifications.email.username && Config.notifications.email.password) {
sendEmail(cartUrl);
}
if (Config.notifications.slack.channel && Config.notifications.slack.token) {
sendSlackMessage(cartUrl);
}
if (Config.notifications.phone.number) {
const carrier = Config.notifications.phone.carrier.toLowerCase();
if (carrier && Config.notifications.phone.availableCarriers.has(carrier)) {
sendSMS(cartUrl);
}
}
if (Config.notifications.playSound) {
playSound();
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ const channel = Config.notifications.slack.channel;
const token = Config.notifications.slack.token; const token = Config.notifications.slack.token;
const web = new WebClient(token); const web = new WebClient(token);
export default function sendSlackMessage(text: string) { export function sendSlackMessage(text: string) {
(async () => { (async () => {
try { try {
const result = await web.chat.postMessage({text, channel}); const result = await web.chat.postMessage({text, channel});
+1 -1
View File
@@ -20,7 +20,7 @@ const mailOptions: Mail.Options = {
subject subject
}; };
export default function sendSMS(text: string) { export function sendSMS(text: string) {
mailOptions.text = text; mailOptions.text = text;
transporter.sendMail(mailOptions, (error, info) => { transporter.sendMail(mailOptions, (error, info) => {
+1 -1
View File
@@ -6,7 +6,7 @@ import * as fs from 'fs';
const notificationSound = './resources/sounds/' + Config.notifications.playSound; const notificationSound = './resources/sounds/' + Config.notifications.playSound;
const player = playerLib(); const player = playerLib();
export default function playSound() { export function playSound() {
// Check if file exists // Check if file exists
fs.access(notificationSound, fs.constants.F_OK, err => { fs.access(notificationSound, fs.constants.F_OK, err => {
if (err) { if (err) {
+2 -30
View File
@@ -1,30 +1,2 @@
import {BestBuy} from './bestbuy'; export * from './lookup';
import {BAndH} from './bandh'; export * from './out-of-stock';
import {Evga} from './evga';
import {NewEgg} from './newegg';
import {Nvidia} from './nvidia';
import {Amazon} from './amazon';
import {MicroCenter} from './microcenter';
import {Config} from '../config';
const masterList = new Map([
['amazon', Amazon],
['bestbuy', BestBuy],
['bandh', BAndH],
['evga', Evga],
['microcenter', MicroCenter],
['newegg', NewEgg],
['nvidia', Nvidia]
]);
const list = new Map();
const storeArray = Config.stores.split(',');
for (const name of storeArray) {
list.set(name, masterList.get(name));
}
export const Stores = Array.from(list.values());
export * from './store';
+66
View File
@@ -0,0 +1,66 @@
import puppeteer from 'puppeteer';
import {Config} from '../config';
import {Logger} from '../logger';
import open from 'open';
import {Store} from './model';
import {sendNotification} from '../notification';
import {isOutOfStock} from './out-of-stock';
/**
* Responsible for looking up information about a each product within
* a `Store`. It's important that we ignore `no-await-in-loop` here
* because we don't want to get rate limited within the same store.
*
* @param store Vendor of graphics cards.
*/
export async function lookup(store: Store) {
/* eslint-disable no-await-in-loop */
for (const link of store.links) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.setDefaultNavigationTimeout(Config.page.navigationTimeout);
await page.setUserAgent(Config.page.userAgent);
await page.setViewport({
height: Config.page.height,
width: Config.page.width
});
const graphicsCard = `${link.brand} ${link.model}`;
try {
await page.goto(link.url, {waitUntil: 'networkidle0'});
} catch {
Logger.error(`✖ [${store.name}] ${graphicsCard} skipping; timed out`);
await browser.close();
return;
}
const bodyHandle = await page.$('body');
const textContent = await page.evaluate(body => body.textContent, bodyHandle);
Logger.debug(textContent);
if (isOutOfStock(textContent, link.oosLabels)) {
Logger.info(`✖ [${store.name}] ${graphicsCard} is still out of stock`);
} else {
Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`);
Logger.info(link.url);
if (Config.page.capture === 'true') {
Logger.debug(' saving screenshot');
await page.screenshot({path: `success-${Date.now()}.png`});
}
const givenUrl = store.cartUrl ? store.cartUrl : link.url;
if (Config.openBrowser === 'true') {
await open(givenUrl);
}
sendNotification(givenUrl);
}
await browser.close();
}
/* eslint-enable no-await-in-loop */
}
+30
View File
@@ -0,0 +1,30 @@
import {BestBuy} from './bestbuy';
import {BAndH} from './bandh';
import {Evga} from './evga';
import {NewEgg} from './newegg';
import {Amazon} from './amazon';
import {MicroCenter} from './microcenter';
import {Config} from '../../config';
import {Nvidia} from "./nvidia";
const masterList = new Map([
['amazon', Amazon],
['bestbuy', BestBuy],
['bandh', BAndH],
['evga', Evga],
['microcenter', MicroCenter],
['newegg', NewEgg],
['nvidia', Nvidia]
]);
const list = new Map();
const storeArray = Config.stores.split(',');
for (const name of storeArray) {
list.set(name, masterList.get(name));
}
export const Stores = Array.from(list.values());
export * from './store';
+10
View File
@@ -0,0 +1,10 @@
/**
* Checks if DOM has any out-of-stock related text.
*
* @param domText Complete DOM of website.
* @param oosLabels Out-of-stock labels.
*/
export function isOutOfStock(domText: string, oosLabels: string[]) {
const domTextLowerCase = domText.toLowerCase();
return oosLabels.some(label => domTextLowerCase.includes(label));
}