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`;