UNPKG

@splode/obake

Version:

Check merchants for deals.

143 lines (120 loc) 3.78 kB
import chalk from "chalk"; import puppeteer, { Browser, Page } from "puppeteer"; import Notifier from "src/message/Notifier"; import Logger from "../Logger"; import Store from "../Store"; import { elide, parsePrice } from "../strings"; import Good from "./Good"; export default abstract class Merchant { protected goods: Good[] = []; protected log: Logger | null; protected notifier: Notifier | null; protected store: Store; public browser: Browser | null = null; public constructor() { this.store = Store.get(); this.notifier = this.store.notifier; this.log = this.store.logger; } public abstract get prettyName(): string; public abstract priceCheck(page: Page, good: Good): Promise<void>; public get isHeadless(): boolean { return true; } public get name(): string { return this.prettyName.toLowerCase().replace(" ", ""); } public async checkGoods(): Promise<void> { if (this.goods.length <= 0) return; if (!this.hasActiveGood()) return; const verbose = this.store.config?.verbose; this.browser = await puppeteer .launch({ args: [!this.isHeadless ? "--window-position=-3860,0" : ""], headless: this.isHeadless, }) .catch((err) => { throw err; }); // this.store.addBrowserToPool(browser) const check = async (good: Good) => { if (!good.disabled) { const page = await this.browser?.newPage().catch((err) => { throw err; }); if (page) { if (verbose) { this.log?.info(`checking for ${good.name} at ${good.URL}...`); } await this.priceCheck(page, good) .catch((err) => { this.log?.error(err.message); }) .finally(async () => { await page.close().catch((err) => { this.log?.error(err.message); throw err; }); }); } } else { if (verbose) { this.log?.info(`skipping due to disabled configuration: ${good.URL}`); } } }; await Promise.all(this.goods.map(check)) .catch((err) => { this.log?.error(err.message); }) .finally(async () => { await this.browser?.close().catch((err) => { throw err; }); }); } public addGoods(...goods: Good[]): void { goods.forEach((g) => this.goods.push(g)); } public async close(): Promise<void> { if (this.browser && this.browser.isConnected) { await this.browser.close().catch((err) => { throw err; }); } } protected parsePrice(ps: string): number { return parsePrice(ps.trim()); } protected handleUnavailable(good: Good): void { this.log?.info( `${elide(good.name)} ${chalk.yellow("unavailable")} for purchase at ${ this.prettyName }: ${good.URL}` ); } protected async handleDiscount(price: number, good: Good): Promise<void> { const msg = good.getDiscountText(price); this.log?.info(chalk.green(msg)); await this.notifier?.send(msg); } protected handleFoundPrice(price: number, good: Good): void { this.log?.info(good.getFoundPriceText(price)); } protected handleNotFoundPrice(good: Good): void { this.log?.warn(good.getNotFoundPriceText()); } protected responseError(res: puppeteer.HTTPResponse, good: Good): Error { return new Error( `request failed with status code ${chalk.yellow(res.status())}: ${ good.URL }` ); } protected requestError(err: Error, good: Good): Error { return new Error(`request to ${good.URL} failed: ${err.message}`); } protected hasActiveGood(): boolean { return this.goods.some((good) => !good.disabled); } }