diff --git a/README.md b/README.md index fd8bf01..912965b 100644 --- a/README.md +++ b/README.md @@ -58,16 +58,16 @@ At any point you want the program to stop, use Ctrl + C. ### Customization -To customize `nvidia-snatcher`, make a copy of `.env-example` as `.env` and make any changes to your liking. _Note that all environment variables are **optional**._ +To customize `nvidia-snatcher`, make a copy of `.env-example` as `.env` and make any changes to your liking. _All environment variables are **optional**._ Here is a list of variables that you can use to customize your newly copied `.env` file: | **Environment variable** | **Description** | **Notes** | |:---:|---|---| | `BROWSER_TRUSTED` | Skip Chromium Sandbox | Useful for containerized environments, default: `false` | -| `DESKTOP_NOTIFICATIONS` | Display desktop notifications using [node-notifier](https://www.npmjs.com/package/node-notifier); optional | Default: `false` | -| `DISCORD_NOTIFY_GROUP` | Discord group you would like to notify; optional | E.g.: `<@2834729847239842>` | -| `DISCORD_WEB_HOOK` | Discord Web Hook URL | | +| `DESKTOP_NOTIFICATIONS` | Display desktop notifications using [node-notifier](https://www.npmjs.com/package/node-notifier) | Default: `false` | +| `DISCORD_NOTIFY_GROUP` | Discord group you would like to notify | Can be comma separated, use role ID, E.g.: `<@2834729847239842>` | +| `DISCORD_WEB_HOOK` | Discord Web Hook URL | Can be comma separated, use whole webhook URL | | `EMAIL_USERNAME` | Gmail address | E.g.: `jensen.robbed.us@gmail.com` | | `EMAIL_PASSWORD` | Gmail password | See below if you have MFA | | `HEADLESS` | Puppeteer to run headless or not | Debugging related, default: `true` | diff --git a/src/config.ts b/src/config.ts index 2328bfa..3568386 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,12 +6,52 @@ import path from 'path'; config({path: path.resolve(__dirname, '../.env')}); +/** + * Returns environment variable, given array, or default array. + * + * @param environment Interested environment variable. + * @param array Default array. If not set, is `[]`. + */ +function envOrArray(environment: string | undefined, array?: string[]): string[] { + return environment ? environment.split(',') : (array ?? []); +} + +/** + * Returns environment variable, given boolean, or default boolean. + * + * @param environment Interested environment variable. + * @param boolean Default boolean. If not set, is `true`. + */ +function envOrBoolean(environment: string | undefined, boolean?: boolean): boolean { + return environment ? environment === 'true' : (boolean ?? true); +} + +/** + * Returns environment variable, given string, or default string. + * + * @param environment Interested environment variable. + * @param string Default string. If not set, is `''`. + */ +function envOrString(environment: string | undefined, string?: string): string { + return environment ? environment : (string ?? ''); +} + +/** + * Returns environment variable, given number, or default number. + * + * @param environment Interested environment variable. + * @param number Default number. If not set, is `0`. + */ +function envOrNumber(environment: string | undefined, number?: number): number { + return Number(environment ?? (number ?? 0)); +} + const browser = { - isHeadless: process.env.HEADLESS ? process.env.HEADLESS === 'true' : true, - isTrusted: process.env.BROWSER_TRUSTED ? process.env.BROWSER_TRUSTED === 'true' : false, - maxSleep: Number(process.env.PAGE_SLEEP_MAX ?? 10000), - minSleep: Number(process.env.PAGE_SLEEP_MIN ?? 5000), - open: process.env.OPEN_BROWSER ? process.env.OPEN_BROWSER === 'true' : true + isHeadless: envOrBoolean(process.env.HEADLESS), + isTrusted: envOrBoolean(process.env.BROWSER_TRUSTED, false), + maxSleep: envOrNumber(process.env.PAGE_SLEEP_MAX, 10000), + minSleep: envOrNumber(process.env.PAGE_SLEEP_MIN, 5000), + open: envOrBoolean(process.env.OPEN_BROWSER) }; const logLevel = process.env.LOG_LEVEL ?? 'info'; @@ -19,12 +59,12 @@ const logLevel = process.env.LOG_LEVEL ?? 'info'; const notifications = { desktop: process.env.DESKTOP_NOTIFICATIONS === 'true', discord: { - notifyGroup: process.env.DISCORD_NOTIFY_GROUP ?? '', - webHookUrl: process.env.DISCORD_WEB_HOOK ?? '' + notifyGroup: envOrArray(process.env.DISCORD_NOTIFY_GROUP), + webHookUrl: envOrArray(process.env.DISCORD_WEB_HOOK) }, email: { - password: process.env.EMAIL_PASSWORD ?? '', - username: process.env.EMAIL_USERNAME ?? '' + password: envOrString(process.env.EMAIL_PASSWORD), + username: envOrString(process.env.EMAIL_USERNAME) }, phone: { availableCarriers: new Map([ @@ -36,47 +76,46 @@ const notifications = { ['tmobile', 'tmomail.net'], ['verizon', 'vtext.com'] ]), - carrier: process.env.PHONE_CARRIER ?? '', - number: process.env.PHONE_NUMBER ?? '' + carrier: envOrString(process.env.PHONE_CARRIER), + number: envOrString(process.env.PHONE_NUMBER) }, - playSound: process.env.PLAY_SOUND ?? '', - pushBulletApiKey: process.env.PUSHBULLET ?? '', + playSound: envOrString(process.env.PLAY_SOUND), + pushBulletApiKey: envOrString(process.env.PUSHBULLET), pushover: { - token: process.env.PUSHOVER_TOKEN ?? '', - username: process.env.PUSHOVER_USER ?? '' + token: envOrString(process.env.PUSHOVER_TOKEN), + username: envOrString(process.env.PUSHOVER_USER) }, slack: { - channel: process.env.SLACK_CHANNEL ?? '', - token: process.env.SLACK_TOKEN ?? '' + channel: envOrString(process.env.SLACK_CHANNEL), + token: envOrString(process.env.SLACK_TOKEN) }, telegram: { - accessToken: process.env.TELEGRAM_ACCESS_TOKEN ?? '', - chatId: process.env.TELEGRAM_CHAT_ID ?? '' + accessToken: envOrString(process.env.TELEGRAM_ACCESS_TOKEN), + chatId: envOrString(process.env.TELEGRAM_CHAT_ID) }, - test: process.env.NOTIFICATION_TEST === 'true', twitter: { - accessTokenKey: process.env.TWITTER_ACCESS_TOKEN_KEY ?? '', - accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET ?? '', - consumerKey: process.env.TWITTER_CONSUMER_KEY ?? '', - consumerSecret: process.env.TWITTER_CONSUMER_SECRET ?? '', - tweetTags: process.env.TWITTER_TWEET_TAGS ?? '' + accessTokenKey: envOrString(process.env.TWITTER_ACCESS_TOKEN_KEY), + accessTokenSecret: envOrString(process.env.TWITTER_ACCESS_TOKEN_SECRET), + consumerKey: envOrString(process.env.TWITTER_CONSUMER_KEY), + consumerSecret: envOrString(process.env.TWITTER_CONSUMER_SECRET), + tweetTags: envOrString(process.env.TWITTER_TWEET_TAGS) } }; const page = { - capture: process.env.SCREENSHOT ? process.env.SCREENSHOT === 'true' : 'true', height: 1080, - inStockWaitTime: Number(process.env.IN_STOCK_WAIT_TIME ?? 0), - navigationTimeout: Number(process.env.PAGE_TIMEOUT ?? 30000), - userAgent: process.env.USER_AGENT ?? 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36', + inStockWaitTime: envOrNumber(process.env.IN_STOCK_WAIT_TIME), + navigationTimeout: envOrNumber(process.env.PAGE_TIMEOUT, 30000), + screenshot: envOrBoolean(process.env.SCREENSHOT), + userAgent: envOrString(process.env.USER_AGENT, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'), width: 1920 }; const store = { - country: process.env.COUNTRY ?? 'usa', - showOnlyBrands: process.env.SHOW_ONLY_BRANDS ? process.env.SHOW_ONLY_BRANDS.split(',') : [], - showOnlySeries: process.env.SHOW_ONLY_SERIES ? process.env.SHOW_ONLY_SERIES.split(',') : ['3070', '3080', '3090'], - stores: process.env.STORES ? process.env.STORES.split(',') : ['nvidia'] + country: envOrString(process.env.COUNTRY, 'usa'), + showOnlyBrands: envOrArray(process.env.SHOW_ONLY_BRANDS), + showOnlySeries: envOrArray(process.env.SHOW_ONLY_SERIES, ['3070', '3080', '3090']), + stores: envOrArray(process.env.STORES, ['nvidia']) }; export const Config = { diff --git a/src/notification/discord.ts b/src/notification/discord.ts index 3918f77..0c5601b 100644 --- a/src/notification/discord.ts +++ b/src/notification/discord.ts @@ -3,7 +3,7 @@ import {MessageBuilder, Webhook} from 'discord-webhook-node'; import {Config} from '../config'; import {Logger} from '../logger'; -const hook = new Webhook(Config.notifications.discord.webHookUrl); +const hooks = Config.notifications.discord.webHookUrl; const notifyGroup = Config.notifications.discord.notifyGroup; export function sendDiscordMessage(link: Link, store: Store) { @@ -17,12 +17,18 @@ export function sendDiscordMessage(link: Link, store: Store) { embed.addField('Model', link.model, true); if (notifyGroup) { - embed.setText(notifyGroup); + embed.setText(notifyGroup.join(' ')); } embed.setColor(0x76B900); embed.setTimestamp(); - await hook.send(embed); + + const promises = []; + for (const hook of hooks) { + promises.push(new Webhook(hook).send(embed)); + } + + await Promise.all(promises); Logger.info('✔ discord message sent'); } catch (error) { diff --git a/src/store/lookup.ts b/src/store/lookup.ts index 1bb49f2..6e4e57a 100644 --- a/src/store/lookup.ts +++ b/src/store/lookup.ts @@ -39,6 +39,7 @@ function filterSeries(series: Link['series']) { * 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 browser Puppeteer browser. * @param store Vendor of graphics cards. */ @@ -94,7 +95,7 @@ async function lookupCard(browser: Browser, store: Store, page: Page, link: Link }, 1000 * Config.page.inStockWaitTime); } - if (Config.page.capture) { + if (Config.page.screenshot) { Logger.debug('ℹ saving screenshot'); link.screenshot = `success-${Date.now()}.png`;