mirror of
https://github.com/opelly27/streetmerchant.git
synced 2026-05-20 05:17:35 +00:00
refactor: lookup, rm defaults (#69)
prep work for #38 Signed-off-by: Jef LeCompte <jeffreylec@gmail.com>
This commit is contained in:
+3
-74
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 +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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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});
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -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';
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user