diff --git a/src/index.ts b/src/index.ts index 5dbbf21..57c4d4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,8 @@ import {Config} from './config'; -import {Store, Stores} from './store'; -import puppeteer from 'puppeteer'; -import open from 'open'; -import sendNotification from './notification'; +import {Stores} from './store/model'; import {Logger} from './logger'; +import {sendNotification} from './notification'; +import {lookup} from './store'; /** * Starts the bot. @@ -21,76 +20,6 @@ async function main() { 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. */ diff --git a/src/notification/email.ts b/src/notification/email.ts index 9090e00..6a104e7 100644 --- a/src/notification/email.ts +++ b/src/notification/email.ts @@ -20,7 +20,7 @@ const mailOptions: Mail.Options = { subject }; -export default function sendEmail(text: string) { +export function sendEmail(text: string) { mailOptions.text = text; transporter.sendMail(mailOptions, (error, info) => { diff --git a/src/notification/index.ts b/src/notification/index.ts index 34a6f69..d9b217c 100644 --- a/src/notification/index.ts +++ b/src/notification/index.ts @@ -1,26 +1 @@ -import {Config} from '../config'; -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(); - } -} +export * from './notification'; diff --git a/src/notification/notification.ts b/src/notification/notification.ts new file mode 100644 index 0000000..1eee804 --- /dev/null +++ b/src/notification/notification.ts @@ -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(); + } +} diff --git a/src/notification/slack.ts b/src/notification/slack.ts index a4d8bfa..582eab5 100644 --- a/src/notification/slack.ts +++ b/src/notification/slack.ts @@ -6,7 +6,7 @@ const channel = Config.notifications.slack.channel; const token = Config.notifications.slack.token; const web = new WebClient(token); -export default function sendSlackMessage(text: string) { +export function sendSlackMessage(text: string) { (async () => { try { const result = await web.chat.postMessage({text, channel}); diff --git a/src/notification/sms.ts b/src/notification/sms.ts index 30c792d..3c16a3c 100644 --- a/src/notification/sms.ts +++ b/src/notification/sms.ts @@ -20,7 +20,7 @@ const mailOptions: Mail.Options = { subject }; -export default function sendSMS(text: string) { +export function sendSMS(text: string) { mailOptions.text = text; transporter.sendMail(mailOptions, (error, info) => { diff --git a/src/notification/sound.ts b/src/notification/sound.ts index ea2bfa3..3376929 100644 --- a/src/notification/sound.ts +++ b/src/notification/sound.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; const notificationSound = './resources/sounds/' + Config.notifications.playSound; const player = playerLib(); -export default function playSound() { +export function playSound() { // Check if file exists fs.access(notificationSound, fs.constants.F_OK, err => { if (err) { diff --git a/src/store/index.ts b/src/store/index.ts index 0a29abf..ab2fb73 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,30 +1,2 @@ -import {BestBuy} from './bestbuy'; -import {BAndH} from './bandh'; -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'; +export * from './lookup'; +export * from './out-of-stock'; diff --git a/src/store/lookup.ts b/src/store/lookup.ts new file mode 100644 index 0000000..a5931e4 --- /dev/null +++ b/src/store/lookup.ts @@ -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 */ +} diff --git a/src/store/amazon.ts b/src/store/model/amazon.ts similarity index 100% rename from src/store/amazon.ts rename to src/store/model/amazon.ts diff --git a/src/store/bandh.ts b/src/store/model/bandh.ts similarity index 100% rename from src/store/bandh.ts rename to src/store/model/bandh.ts diff --git a/src/store/bestbuy.ts b/src/store/model/bestbuy.ts similarity index 100% rename from src/store/bestbuy.ts rename to src/store/model/bestbuy.ts diff --git a/src/store/evga.ts b/src/store/model/evga.ts similarity index 100% rename from src/store/evga.ts rename to src/store/model/evga.ts diff --git a/src/store/model/index.ts b/src/store/model/index.ts new file mode 100644 index 0000000..8f05870 --- /dev/null +++ b/src/store/model/index.ts @@ -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'; diff --git a/src/store/microcenter.ts b/src/store/model/microcenter.ts similarity index 100% rename from src/store/microcenter.ts rename to src/store/model/microcenter.ts diff --git a/src/store/newegg.ts b/src/store/model/newegg.ts similarity index 100% rename from src/store/newegg.ts rename to src/store/model/newegg.ts diff --git a/src/store/nvidia.ts b/src/store/model/nvidia.ts similarity index 100% rename from src/store/nvidia.ts rename to src/store/model/nvidia.ts diff --git a/src/store/store.ts b/src/store/model/store.ts similarity index 100% rename from src/store/store.ts rename to src/store/model/store.ts diff --git a/src/store/out-of-stock.ts b/src/store/out-of-stock.ts new file mode 100644 index 0000000..86d2010 --- /dev/null +++ b/src/store/out-of-stock.ts @@ -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)); +}