Skip to content

Commit

Permalink
Merge branch 'main' into pushbullet
Browse files Browse the repository at this point in the history
  • Loading branch information
jef authored Sep 23, 2020
2 parents 6bc9563 + 6608a79 commit 26f9daf
Show file tree
Hide file tree
Showing 23 changed files with 289 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ COUNTRY="usa"
SCREENSHOT="true"
TELEGRAM_ACCESS_TOKEN=""
TELEGRAM_CHAT_ID="1234"
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN_KEY=
TWITTER_ACCESS_TOKEN_SECRET=
TWITTER_TWEET_TAGS=
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ Here is a list of variables that you can use to customize your newly copied `.en
| `TELEGRAM_ACCESS_TOKEN` | Telegram access token |
| `TELEGRAM_CHAT_ID` | Telegram chat ID |
| `USER_AGENT` | Custom User-Agent header for HTTP requests | Default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36` |
| `TWITTER_CONSUMER_KEY` | Twitter Consumer Key | Generate all Twitter keys at: https://developer.twitter.com/ |
| `TWITTER_CONSUMER_SECRET` | Twitter Consumer Secret |
| `TWITTER_ACCESS_TOKEN_KEY` | Twitter Token Key |
| `TWITTER_ACCESS_TOKEN_SECRET` | Twitter Token Secret |
| `TWITTER_TWEET_TAGS` | Optional list of hashtags to append to the tweet message | Eg: "`#NVIDIA` `#NVIDIAINSTOCK`" |

> :point_right: If you have multi-factor authentication (MFA), you will need to create an [app password](https://myaccount.google.com/apppasswords) and use this instead of your Gmail password.
Expand Down Expand Up @@ -145,6 +150,7 @@ Here is a list of variables that you can use to customize your newly copied `.en
| ireland | `` | | |
| italy | `` | | |
| luxembourg | `` | | Nvidia supports debug |
| netherlands | `` | | Nvidia supports debug |
| poland | `` | | |
| portugal | `` | | |
| russia | | | Missing all IDs |
Expand Down
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-adblocker": "^2.11.6",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"pushover-notifications": "^1.2.2",
"pushbullet": "^2.4.0",
"pushover-notifications": "^1.2.2",
"twitter": "^1.7.1",
"winston": "^3.3.3"
},
"devDependencies": {
Expand All @@ -42,6 +43,7 @@
"@types/node-notifier": "^8.0.0",
"@types/nodemailer": "^6.4.0",
"@types/puppeteer": "^3.0.2",
"@types/twitter": "^1.7.0",
"discord-webhook-node": "^1.1.8",
"husky": "^4.3.0",
"play-sound": "^1.1.3",
Expand Down
9 changes: 8 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ const notifications = {
accessToken: process.env.TELEGRAM_ACCESS_TOKEN ?? '',
chatId: process.env.TELEGRAM_CHAT_ID ?? ''
},
test: process.env.NOTIFICATION_TEST === 'true'
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 ?? ''
}
};

const page = {
Expand Down
10 changes: 10 additions & 0 deletions src/notification/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {sendPushoverNotification} from './pushover';
import {sendSMS} from './sms';
import {sendSlackMessage} from './slack';
import {sendTelegramMessage} from './telegram';
import {sendTweet} from './twitter';

const notifications = Config.notifications;

Expand Down Expand Up @@ -51,4 +52,13 @@ export function sendNotification(cartUrl: string, link: Link) {
if (notifications.desktop) {
sendDesktopNotification(cartUrl, link);
}

if (
notifications.twitter.accessTokenKey &&
notifications.twitter.accessTokenSecret &&
notifications.twitter.consumerKey &&
notifications.twitter.consumerSecret
) {
sendTweet(cartUrl, link);
}
}
29 changes: 29 additions & 0 deletions src/notification/twitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Config} from '../config';
import {Link} from '../store/model';
import {Logger} from '../logger';
import Twitter from 'twitter';

const twitter = Config.notifications.twitter;

const client = new Twitter({
access_token_key: twitter.accessTokenKey,
access_token_secret: twitter.accessTokenSecret,
consumer_key: twitter.consumerKey,
consumer_secret: twitter.consumerSecret
});

export function sendTweet(cartUrl: string, link: Link) {
let status = `🛎️ Stock Notification: ${link.brand} ${link.model}\n${cartUrl}`;

if (twitter.tweetTags) {
status += `\n\n${twitter.tweetTags}`;
}

client.post('statuses/update', {status}, err => {
if (err) {
Logger.error(err);
} else {
Logger.info(`↗ twitter notification sent: ${cartUrl}`);
}
});
}
128 changes: 81 additions & 47 deletions src/store/lookup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Browser, Response} from 'puppeteer';
import {Browser, Page, Response} from 'puppeteer';
import {Link, Store} from './model';
import {closePage, delay, getSleepTime} from '../util';
import {Config} from '../config';
import {Logger} from '../logger';
import {Store} from './model';
import {includesLabels} from './includes-labels';
import open from 'open';
import {sendNotification} from '../notification';
Expand Down Expand Up @@ -57,63 +57,97 @@ async function lookup(browser: Browser, store: Store) {
page.setDefaultNavigationTimeout(Config.page.navigationTimeout);
await page.setUserAgent(Config.page.userAgent);

const graphicsCard = `${link.brand} ${link.model} ${link.series}`;

let response: Response | null;
try {
response = await page.goto(link.url, {waitUntil: 'networkidle0'});
} catch {
Logger.error(`✖ [${store.name}] ${graphicsCard} skipping; timed out`);
await closePage(page);
continue;
await lookupCard(browser, store, page, link);
} catch (error) {
Logger.error(`✖ [${store.name}] ${link.brand} ${link.model} - ${error.message as string}`);
}

const bodyHandle = await page.$('body');
const textContent = await page.evaluate(body => body.textContent, bodyHandle);
await closePage(page);
}
/* eslint-enable no-await-in-loop */
}

Logger.debug(textContent);
async function lookupCard(browser: Browser, store: Store, page: Page, link: Link) {
const response: Response | null = await page.goto(link.url, {waitUntil: 'networkidle0'});
const graphicsCard = `${link.brand} ${link.model}`;

if (await lookupCardInStock(store, page)) {
Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`);
Logger.info(link.url);
if (Config.page.inStockWaitTime) {
inStock[store.name] = true;
setTimeout(() => {
inStock[store.name] = false;
}, 1000 * Config.page.inStockWaitTime);
}

if (includesLabels(textContent, store.labels.outOfStock)) {
Logger.info(`✖ [${store.name}] still out of stock: ${graphicsCard}`);
} else if (store.labels.bannedSeller && includesLabels(textContent, store.labels.bannedSeller)) {
Logger.warn(`✖ [${store.name}] banned seller detected: ${graphicsCard}. skipping...`);
} else if (store.labels.captcha && includesLabels(textContent, store.labels.captcha)) {
Logger.warn(`✖ [${store.name}] CAPTCHA from: ${graphicsCard}. Waiting for a bit with this store...`);
await delay(getSleepTime());
} else if (response && response.status() === 429) {
Logger.warn(`✖ [${store.name}] Rate limit exceeded: ${graphicsCard}`);
} else {
Logger.info(`🚀🚀🚀 [${store.name}] ${graphicsCard} IN STOCK 🚀🚀🚀`);
Logger.info(link.url);
if (Config.page.inStockWaitTime) {
inStock[store.name] = true;
setTimeout(() => {
inStock[store.name] = false;
}, 1000 * Config.page.inStockWaitTime);
}
if (Config.page.capture) {
Logger.debug('ℹ saving screenshot');
link.screenshot = `success-${Date.now()}.png`;
await page.screenshot({path: link.screenshot});
}

const givenUrl = link.cartUrl ? link.cartUrl : link.url;

if (Config.page.capture) {
Logger.debug('ℹ saving screenshot');
link.screenshot = `success-${Date.now()}.png`;
await page.screenshot({path: link.screenshot});
if (Config.browser.open) {
if (link.openCartAction === undefined) {
await open(givenUrl);
} else {
link.openCartAction(browser);
}
}

let givenUrl = link.cartUrl ? link.cartUrl : link.url;
sendNotification(givenUrl, link);
return;
}

if (Config.browser.open) {
if (link.openCartAction === undefined) {
await open(givenUrl);
} else {
givenUrl = await link.openCartAction(browser);
}
}
if (await lookupPageHasCaptcha(store, page)) {
Logger.warn(`✖ [${store.name}] CAPTCHA from: ${graphicsCard}. Waiting for a bit with this store...`);
await delay(getSleepTime());
return;
}

sendNotification(givenUrl, link);
}
if (response && response.status() === 429) {
Logger.warn(`✖ [${store.name}] Rate limit exceeded: ${graphicsCard}`);
return;
}

await closePage(page);
Logger.info(`✖ [${store.name}] still out of stock: ${graphicsCard}`);
}

async function lookupCardInStock(store: Store, page: Page) {
const stockHandle = await page.$(store.labels.inStock.container);

const visible = await page.evaluate(element => element && element.offsetWidth > 0 && element.offsetHeight > 0, stockHandle);
if (!visible) {
return false;
}
/* eslint-enable no-await-in-loop */

const stockContent = await page.evaluate(element => element.outerHTML, stockHandle);

Logger.debug(stockContent);

if (includesLabels(stockContent, store.labels.inStock.text)) {
return true;
}

return false;
}

async function lookupPageHasCaptcha(store: Store, page: Page) {
if (!store.labels.captcha) {
return false;
}

const captchaHandle = await page.$(store.labels.captcha.container);
const captchaContent = await page.evaluate(element => element.textContent, captchaHandle);

if (includesLabels(captchaContent, store.labels.captcha.text)) {
return true;
}

return false;
}

export async function tryLookupAndLoop(browser: Browser, store: Store) {
Expand Down
6 changes: 4 additions & 2 deletions src/store/model/adorama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {Store} from './store';

export const Adorama: Store = {
labels: {
captcha: ['please verify you are a human'],
outOfStock: ['temporarily not available', 'out of stock']
inStock: {
container: '.buy-section.purchase',
text: ['add to cart']
}
},
links: [
{
Expand Down
10 changes: 8 additions & 2 deletions src/store/model/amazon-ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import {Store} from './store';

export const AmazonCa: Store = {
labels: {
captcha: ['enter the characters you see below'],
outOfStock: ['currently unavailable']
captcha: {
container: 'body',
text: ['enter the characters you see below']
},
inStock: {
container: '#desktop_buybox',
text: ['add to cart']
}
},
links: [
{
Expand Down
11 changes: 8 additions & 3 deletions src/store/model/amazon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import {Store} from './store';

export const Amazon: Store = {
labels: {
bannedSeller: ['sports authentics', 'raccoon capitalist', 'gigaparts'],
captcha: ['enter the characters you see below'],
outOfStock: ['currently unavailable', 'available from these sellers']
captcha: {
container: 'body',
text: ['enter the characters you see below']
},
inStock: {
container: '#desktop_buybox',
text: ['add to cart']
}
},
links: [
{
Expand Down
Loading

0 comments on commit 26f9daf

Please sign in to comment.