├── CONTRIBUTING.md ├── .node-version ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── workflows │ ├── pr-lint.yaml │ ├── documentation.yaml │ ├── ci.yaml │ ├── nightly-release.yaml │ └── release.yaml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yaml │ └── bug-report.yaml └── pull_request_template.md ├── docs ├── changelog.md ├── reference │ ├── terraform.md │ ├── proxy.md │ └── application.md ├── assets │ └── images │ │ ├── streetmerchant-logo.png │ │ ├── streetmerchant-banner.png │ │ ├── streetmerchant-github.png │ │ ├── streetmerchant-herokuapp.jpg │ │ ├── streetmerchan-herokunewapp.jpg │ │ ├── streetmerchant-herokudynos.jpg │ │ └── streetmerchant-herokubuildpacks.png ├── javascripts │ └── tables.js ├── about.md ├── overrides │ └── main.html ├── index.md ├── help │ └── troubleshoot.md └── faq.md ├── .eslintignore ├── src ├── store │ ├── index.ts │ ├── timestamp-url-parameter.ts │ ├── model │ │ ├── xbox.ts │ │ ├── ao.ts │ │ ├── bt.ts │ │ ├── johnlewis.ts │ │ ├── gamestop-ie.ts │ │ ├── harveynorman-ie.ts │ │ ├── asus-es.ts │ │ ├── gamestop-it.ts │ │ ├── shopto.ts │ │ ├── tesco-ie.ts │ │ ├── argos-ie.ts │ │ ├── microsoft.ts │ │ ├── microsoft-ca.ts │ │ ├── gamestop-de.ts │ │ ├── lmc.ts │ │ ├── smythstoys-ie.ts │ │ ├── corsair.ts │ │ ├── antonline.ts │ │ ├── bpmpower.ts │ │ ├── euronics.ts │ │ ├── comet.ts │ │ ├── thewarehouse.ts │ │ ├── unieuro.ts │ │ ├── noelleeming.ts │ │ ├── medimax.ts │ │ ├── argos.ts │ │ ├── mightyape.ts │ │ ├── smythstoys.ts │ │ ├── officedepot.ts │ │ ├── gamestop-ca.ts │ │ ├── toysrus.ts │ │ ├── corsair-uk.ts │ │ ├── game.ts │ │ ├── spielegrotte.ts │ │ ├── helpers │ │ │ └── backoff.ts │ │ ├── amd-ca.ts │ │ ├── walmart-ca.ts │ │ ├── expert.ts │ │ ├── target.ts │ │ ├── allneeds.ts │ │ ├── harristechnology.ts │ │ ├── ubiquiti.ts │ │ ├── playstation.ts │ │ ├── amazon-de-warehouse.ts │ │ ├── euronics-de.ts │ │ ├── evga-eu.ts │ │ ├── elcorteingles.ts │ │ ├── nvidia-gb.ts │ │ ├── nvidia-es.ts │ │ ├── nvidia-fr.ts │ │ ├── nvidia-de.ts │ │ ├── amd-it.ts │ │ ├── dustinhome-no.ts │ │ ├── aria.ts │ │ ├── pbtech.ts │ │ ├── eprice.ts │ │ ├── centrecom.ts │ │ ├── amd-nl.ts │ │ ├── mediamarkt-at.ts │ │ ├── wellstechnology.ts │ │ ├── walmart.ts │ │ ├── ldlc-italy.ts │ │ ├── storm.ts │ │ ├── box.ts │ │ ├── igame.ts │ │ ├── awd.ts │ │ ├── futurex.ts │ │ ├── very.ts │ │ ├── coolmod.ts │ │ ├── otto.ts │ │ ├── asus-de.ts │ │ └── galaxus.ts │ ├── filter.ts │ └── fetch-links.ts ├── types │ ├── node-pagerduty.d.ts │ ├── top-user-agents.d.ts │ ├── play-sound.d.ts │ └── pushover-notifications.d.ts ├── messaging │ ├── index.ts │ ├── pushbullet.ts │ ├── desktop.ts │ ├── pagerduty.ts │ ├── captcha.ts │ ├── gotify.ts │ ├── telegram.ts │ ├── twitter.ts │ ├── sound.ts │ ├── twilio.ts │ ├── apns.ts │ ├── freemobile.ts │ ├── redis.ts │ ├── streamlabs.ts │ ├── email.ts │ ├── ntfy.ts │ ├── pushover.ts │ ├── notification.ts │ └── sms.ts ├── adblocker.ts └── banner.ts ├── .prettierrc.js ├── web └── favicon.ico ├── .dockerignore ├── test ├── test-blank.ts └── functional │ ├── test-notification.ts │ └── test-captcha.ts ├── nodemon.json ├── .editorconfig ├── .eslintrc.json ├── docker-compose.yml ├── Makefile ├── .gitignore ├── terraform ├── terraform.tfvars ├── provider.tf ├── taskdef.json.template ├── resource-vpc.tf ├── dashboard.json.template ├── variables.tf ├── resource-logging.tf ├── README.md └── resource-ecs.tf ├── tsconfig.json ├── LICENSE ├── helpers └── brandList.ts ├── README.md ├── Dockerfile ├── mkdocs.yml └── package.json /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.18.0 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jef 2 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | docs/ 3 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lookup'; 2 | -------------------------------------------------------------------------------- /docs/reference/terraform.md: -------------------------------------------------------------------------------- 1 | ../../terraform/README.md -------------------------------------------------------------------------------- /src/types/node-pagerduty.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'node-pagerduty'; 2 | -------------------------------------------------------------------------------- /src/types/top-user-agents.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'top-user-agents'; 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jef 2 | custom: ["https://www.paypal.me/jxf"] 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | .vs/ 4 | .vscode/ 5 | build/ 6 | docs/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /test/test-blank.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | assert.strictEqual(true, true); 4 | -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-logo.png -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-banner.png -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-github.png -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-herokuapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-herokuapp.jpg -------------------------------------------------------------------------------- /docs/assets/images/streetmerchan-herokunewapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchan-herokunewapp.jpg -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-herokudynos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-herokudynos.jpg -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "exec": "npx ts-node --files ./src/index", 3 | "ext": "ts", 4 | "watch": [ 5 | "src/", 6 | "dotenv" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/assets/images/streetmerchant-herokubuildpacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jef/streetmerchant/HEAD/docs/assets/images/streetmerchant-herokubuildpacks.png -------------------------------------------------------------------------------- /docs/javascripts/tables.js: -------------------------------------------------------------------------------- 1 | document$.subscribe(function() { 2 | var tables = document.querySelectorAll("article table") 3 | tables.forEach(function(table) { 4 | new Tablesort(table) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off", 5 | "prettier/prettier": [ 6 | "error", 7 | { 8 | "endOfLine": "auto" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | streetmerchant: 4 | image: ghcr.io/jef/streetmerchant:latest 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: streetmerchant 9 | env_file: 10 | - dotenv 11 | -------------------------------------------------------------------------------- /src/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './captcha'; 2 | export * from './notification'; 3 | 4 | export type DMPayloadType = 'text' | 'image'; 5 | 6 | export interface DMPayload { 7 | content: string; // for image type, content is local file path 8 | type: DMPayloadType; 9 | } 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := run 2 | 3 | .PHONY: build 4 | build: 5 | docker-compose build streetmerchant 6 | 7 | .PHONY: run 8 | run: 9 | docker-compose up 10 | 11 | .PHONY: run-detached 12 | run-detached: 13 | docker-compose up -d 14 | 15 | .PHONY: stop 16 | stop: 17 | docker-compose down 18 | -------------------------------------------------------------------------------- /src/store/timestamp-url-parameter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates unique URL param to prevent cached responses (similar to jQuery that Nvidia uses) 3 | * 4 | * @return string in format &=1111111111111 (time since epoch in ms) 5 | */ 6 | export function timestampUrlParameter(): string { 7 | return `&_=${Date.now()}`; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ 3 | .vscode/ 4 | build/ 5 | coverage/ 6 | node_modules/ 7 | 8 | .env 9 | dotenv 10 | *.proxies 11 | success-*.png 12 | screenshots/ 13 | 14 | *.wav 15 | *.mp3 16 | *.flac 17 | *.exe 18 | desktop.ini 19 | 20 | twitch.json 21 | terraform/terraform.tfstate 22 | terraform/terraform.tfstate.backup 23 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | credential_profile = "ps5" 2 | 3 | streetmerchant_env = { 4 | "STORES" = "amazon-uk,game,argos,box,currys,johnlewis,shopto,smythstoys,very,amazon-it,amazon-nl" 5 | "SHOW_ONLY_SERIES" = "sonyps5c,sonyps5de" 6 | "SLACK_TOKEN" = "your slack api token" 7 | "SLACK_CHANNEL" = "your slack channel name" 8 | } 9 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 3.0" 6 | } 7 | } 8 | } 9 | 10 | provider "aws" { 11 | region = var.region 12 | shared_credentials_file = var.credential_file 13 | profile = var.credential_profile 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "outDir": "build", 6 | "resolveJsonModule": true, 7 | "rootDir": ".", 8 | // todo: remove 9 | "skipLibCheck": true, 10 | "lib": ["DOM", "ES2021"] 11 | }, 12 | "include": ["src/**/*.ts", "test/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /terraform/taskdef.json.template: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "${name}-task", 4 | "image": "ghcr.io/jef/streetmerchant:latest", 5 | "cpu": ${cpu}, 6 | "memory": ${memory}, 7 | "essential": true, 8 | "environment": ${environment}, 9 | "logConfiguration": { 10 | "logDriver": "awslogs", 11 | "options": { 12 | "awslogs-group": "${awslogs-group}", 13 | "awslogs-region": "${region}", 14 | "awslogs-stream-prefix": "ecs" 15 | } 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-lint.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request Linter 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | - synchronize 9 | jobs: 10 | lint-pr: 11 | name: Lint pull request title 12 | runs-on: ubuntu-latest 13 | permissions: 14 | pull-requests: write 15 | steps: 16 | - name: Lint pull request title 17 | uses: jef/conventional-commits-pr-action@v1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /test/functional/test-notification.ts: -------------------------------------------------------------------------------- 1 | import open from 'open'; 2 | import {sendNotification} from '../../src/messaging'; 3 | import {config} from '../../src/config'; 4 | import {getTestStore} from '../util'; 5 | 6 | const store = getTestStore(); 7 | const link = store.links[0]; 8 | 9 | /** 10 | * Send test email. 11 | */ 12 | sendNotification(link, store); 13 | 14 | /** 15 | * Open browser. 16 | */ 17 | if (!config.docker && config.browser.open) { 18 | open(link.cartUrl ?? link.url); 19 | open(link.url); 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💡 Have an idea for a new feature? 4 | url: https://github.com/jef/streetmerchant/discussions 5 | about: Create a new idea discussion! 6 | - name: 🙇 Need help with streetmerchant? 7 | url: https://github.com/jef/streetmerchant/discussions 8 | about: Create a new help discussion if it hasn't been asked before! 9 | - name: 💬 Want to talk with others that use streetmerchant? 10 | url: https://discord.gg/gbVY4vB9JF 11 | about: Join our Discord to hangout and talk shop! 12 | -------------------------------------------------------------------------------- /src/store/model/xbox.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Xbox: Store = { 4 | currency: '$', 5 | labels: { 6 | outOfStock: { 7 | container: 8 | '[class="BundleBuilderHeader-module__checkoutButton___3UyEq w-100 bg-light-green btn btn-primary"]', 9 | text: ['Out of stock'], 10 | }, 11 | }, 12 | links: [ 13 | { 14 | brand: 'microsoft', 15 | model: 'xbox series x', 16 | series: 'xboxsx', 17 | url: 'https://www.xbox.com/en-us/configure/8WJ714N3RBTL', 18 | }, 19 | ], 20 | name: 'xbox', 21 | country: 'US', 22 | }; 23 | -------------------------------------------------------------------------------- /docs/reference/proxy.md: -------------------------------------------------------------------------------- 1 | # Proxy 2 | 3 | ## Filename 4 | 5 | Proxy configuration can be set either per store in a file called `storename.proxies` or globally in `global.proxies` in the streetmerchant root directory. 6 | 7 | If both exist, the store specific file will take precedence. 8 | 9 | ## Format 10 | 11 | The format is one proxy per line with the following structure: 12 | `protocol://[user:password@]ip[:port]` 13 | 14 | Supported protocols are `http` and `socks5`. 15 | 16 | Valid examples include: 17 | - `socks5://1.2.3.4:3180` 18 | - `socks5://abcd:efgh@1.2.3.4:5678` 19 | - `http://1.2.3.4:80` 20 | - `http://abcd:efgh@1.2.3.4:8080` 21 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build_deploy: 8 | name: Build and publish documentation 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v3 13 | - name: Setup Python 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: 3.x 17 | - name: Install Python packages 18 | run: pip install mkdocs-material mkdocs-git-revision-date-plugin mkdocs-macros-plugin 19 | - name: Build and publish documentation 20 | run: mkdocs gh-deploy --force 21 | -------------------------------------------------------------------------------- /src/store/model/ao.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AO: Store = { 4 | currency: '£', 5 | labels: { 6 | outOfStock: { 7 | container: 'section.centred-heading-copy strong', 8 | text: ['currently unavailable'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'sony', 14 | model: 'ps5 console', 15 | series: 'sonyps5c', 16 | url: 'https://ao.com/brands/playstation', 17 | }, 18 | { 19 | brand: 'sony', 20 | model: 'ps5 digital', 21 | series: 'sonyps5de', 22 | url: 'https://ao.com/brands/playstation', 23 | }, 24 | ], 25 | name: 'ao', 26 | country: 'UK', 27 | }; 28 | -------------------------------------------------------------------------------- /src/types/play-sound.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'play-sound' { 2 | export interface Options { 3 | players?: string[]; 4 | player?: string; 5 | } 6 | 7 | export type PlayOptions = Record; 8 | 9 | export interface PlaySound { 10 | player: string; 11 | 12 | play: ((file: string, callback: (error: Error) => void) => PlayerProcess) & 13 | (( 14 | file: string, 15 | options: PlayOptions, 16 | callback: (error: Error) => void 17 | ) => PlayerProcess); 18 | } 19 | 20 | export interface PlayerProcess { 21 | kill: () => void; 22 | } 23 | 24 | export default function (options?: Options): PlaySound; 25 | } 26 | -------------------------------------------------------------------------------- /test/functional/test-captcha.ts: -------------------------------------------------------------------------------- 1 | import {handleCaptchaAsync} from '../../src/store/captcha-handler'; 2 | import {getTestStore, launchTestBrowser} from '../util'; 3 | 4 | const store = getTestStore(); 5 | // uncomment to test global default capture type setting 6 | // if (store.labels.captchaHandler) store.labels.captchaHandler.captureType = ''; 7 | 8 | (async () => { 9 | const browser = await launchTestBrowser(); 10 | const page = await browser.newPage(); 11 | page.goto(store.links[1].url, {waitUntil: 'networkidle0'}); 12 | await page.waitForSelector(store.labels.captchaHandler!.challenge); 13 | await handleCaptchaAsync(page, store); 14 | await browser.close(); 15 | })(); 16 | -------------------------------------------------------------------------------- /src/store/model/bt.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const BT: Store = { 4 | currency: '£', 5 | labels: { 6 | outOfStock: { 7 | container: '#cms-component-content-panel-200124986 p', 8 | text: ["We've sold out of our current allocation of PlayStation 5"], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'sony', 14 | model: 'ps5 console', 15 | series: 'sonyps5c', 16 | url: 'https://shop.bt.com/mini-sites/gaming', 17 | }, 18 | { 19 | brand: 'sony', 20 | model: 'ps5 digital', 21 | series: 'sonyps5de', 22 | url: 'https://shop.bt.com/mini-sites/gaming', 23 | }, 24 | ], 25 | name: 'bt', 26 | country: 'UK', 27 | }; 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### Description 6 | 7 | 8 | 9 | 10 | ### Testing 11 | 12 | 13 | 14 | 15 | 16 | ### New dependencies 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/adblocker.ts: -------------------------------------------------------------------------------- 1 | import {Page} from 'puppeteer'; 2 | import {PuppeteerExtraPluginAdblocker} from 'puppeteer-extra-plugin-adblocker'; 3 | 4 | export const adBlocker = new PuppeteerExtraPluginAdblocker({ 5 | blockTrackers: true, 6 | }); 7 | 8 | export async function enableBlockerInPage(page: Page) { 9 | const blockerObject = await adBlocker.getBlocker(); 10 | if (blockerObject.isBlockingEnabled(page)) { 11 | return; 12 | } 13 | 14 | await blockerObject.enableBlockingInPage(page); 15 | } 16 | 17 | export async function disableBlockerInPage(page: Page) { 18 | const blockerObject = await adBlocker.getBlocker(); 19 | if (!blockerObject.isBlockingEnabled(page)) { 20 | return; 21 | } 22 | 23 | await blockerObject.disableBlockingInPage(page); 24 | } 25 | -------------------------------------------------------------------------------- /src/store/model/johnlewis.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const JohnLewis: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '#button--add-to-basket', 8 | text: ['Add to your basket'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'test:brand', 14 | model: 'test:model', 15 | series: 'test:series', 16 | url: 'https://www.johnlewis.com/sony-playstation-5-dualsense-wireless-controller-white/p5192093', 17 | }, 18 | { 19 | brand: 'sony', 20 | model: 'ps5 console', 21 | series: 'sonyps5c', 22 | url: 'https://www.johnlewis.com/sony-playstation-5-console-with-dualsense-controller/white/p5115192', 23 | }, 24 | ], 25 | name: 'johnlewis', 26 | country: 'UK', 27 | }; 28 | -------------------------------------------------------------------------------- /src/store/model/gamestop-ie.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const GamestopIE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '#btnAddToCart', 8 | text: ['add to cart!'], 9 | }, 10 | maxPrice: { 11 | container: 'span.pricetext', 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'microsoft', 17 | model: 'xbox series x', 18 | series: 'xboxsx', 19 | url: 'https://www.gamestop.ie/Xbox%20Series/Games/73034/xbox-series-x-console', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 digital', 24 | series: 'sonyps5de', 25 | url: 'https://www.gamestop.ie/PlayStation%205/Games/72504/playstation-5-console', 26 | }, 27 | ], 28 | name: 'gamestop-ie', 29 | country: 'IE', 30 | }; 31 | -------------------------------------------------------------------------------- /src/store/model/harveynorman-ie.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const HarveyNormanIE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: 'input.btn-action', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '.price', 12 | euroFormat: false, 13 | }, 14 | outOfStock: { 15 | container: '.product-highlight-text', 16 | text: ['SOLD OUT! WATCH THIS SPACE FOR MORE INFORMATION'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'microsoft', 22 | model: 'xbox series x', 23 | series: 'xboxsx', 24 | url: 'https://www.harveynorman.ie/gaming/xbox-series/microsoft-xbox-series-x-console-1tb.html', 25 | }, 26 | ], 27 | name: 'harveynorman-ie', 28 | country: 'IE', 29 | waitUntil: 'domcontentloaded', 30 | }; 31 | -------------------------------------------------------------------------------- /src/messaging/pushbullet.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import PushBullet from '@jef/pushbullet'; 4 | import {config} from '../config'; 5 | 6 | const {pushbullet} = config.notifications; 7 | 8 | export function sendPushbulletNotification(link: Link, store: Store) { 9 | if (pushbullet) { 10 | logger.debug('↗ sending pushbullet message'); 11 | 12 | const pusher = new PushBullet(pushbullet); 13 | 14 | pusher.note( 15 | {}, 16 | Print.inStock(link, store), 17 | link.cartUrl ? link.cartUrl : link.url, 18 | (error: Error) => { 19 | if (error) { 20 | logger.error("✖ couldn't send pushbullet message", error); 21 | } else { 22 | logger.info('✔ pushbullet message sent'); 23 | } 24 | } 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/messaging/desktop.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {config} from '../config'; 4 | import {join} from 'path'; 5 | import notifier from 'node-notifier'; 6 | 7 | const {desktop} = config.notifications; 8 | 9 | export function sendDesktopNotification(link: Link, store: Store) { 10 | if (desktop) { 11 | logger.debug('↗ sending desktop notification'); 12 | (async () => { 13 | notifier.notify({ 14 | icon: join( 15 | __dirname, 16 | '../../../docs/assets/images/streetmerchant-logo.png' 17 | ), 18 | message: link.cartUrl ? link.cartUrl : link.url, 19 | open: link.cartUrl ? link.cartUrl : link.url, 20 | title: Print.inStock(link, store), 21 | }); 22 | 23 | logger.info('✔ desktop notification sent'); 24 | })(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /terraform/resource-vpc.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "main" { 2 | enable_dns_support = true 3 | cidr_block = "10.0.0.0/16" 4 | tags = { 5 | app = "ps5" 6 | } 7 | } 8 | 9 | resource "aws_internet_gateway" "main" { 10 | vpc_id = aws_vpc.main.id 11 | tags = { 12 | app = "ps5" 13 | } 14 | } 15 | 16 | resource "aws_subnet" "aws-subnet" { 17 | vpc_id = aws_vpc.main.id 18 | cidr_block = aws_vpc.main.cidr_block 19 | map_public_ip_on_launch = true 20 | tags = { 21 | app = "ps5" 22 | } 23 | } 24 | 25 | resource "aws_route_table" "main" { 26 | vpc_id = aws_vpc.main.id 27 | route { 28 | cidr_block = "0.0.0.0/0" 29 | gateway_id = aws_internet_gateway.main.id 30 | } 31 | tags = { 32 | app = "ps5" 33 | } 34 | } 35 | 36 | resource "aws_main_route_table_association" "main" { 37 | route_table_id = aws_route_table.main.id 38 | vpc_id = aws_vpc.main.id 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/store/model/asus-es.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AsusEs: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '#product-addtocart-button > span', 8 | text: ['Añadir a la cesta'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'test:brand', 14 | model: 'test:model', 15 | series: 'test:series', 16 | url: 'https://estore.asus.com/es/90mb1680-m0eay0-rog-strix-z590-i-gaming-wifi.html', 17 | }, 18 | { 19 | brand: 'asus', 20 | model: 'tuf', 21 | series: '3080', 22 | url: 'https://estore.asus.com/es/90yv0fb0-m0nm00-tuf-rtx3080-10g-gaming.html', 23 | }, 24 | { 25 | brand: 'asus', 26 | model: 'tuf', 27 | series: '3090', 28 | url: 'https://estore.asus.com/es/90yv0fd0-m0nm00-tuf-rtx3090-24g-gaming.html', 29 | }, 30 | ], 31 | name: 'asus-es', 32 | country: 'ES', 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/model/gamestop-it.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const GamestopIT: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '#btnAddToCart', 8 | text: ['Aggiungi al Carrello'], 9 | }, 10 | maxPrice: { 11 | container: '.buySection .prodPriceCont', 12 | euroFormat: true, 13 | }, 14 | outOfStock: { 15 | container: '.megaButton .buyDisabled', 16 | text: ['Esaurito'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'microsoft', 22 | model: 'xbox series x', 23 | series: 'xboxsx', 24 | url: 'https://www.gamestop.it/XboxSeriesX/Games/132509', 25 | }, 26 | { 27 | brand: 'microsoft', 28 | model: 'xbox series s', 29 | series: 'xboxss', 30 | url: 'https://www.gamestop.it/XboxSeriesX/Games/128220', 31 | }, 32 | ], 33 | name: 'gamestop-it', 34 | country: 'IT', 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/model/shopto.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const ShopTo: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '.orderbox_inventory', 8 | text: ['In Stock'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'test:brand', 14 | model: 'test:model', 15 | series: 'test:series', 16 | url: 'https://www.shopto.net/en/ps5du00-dualsense-controller-playstation-5-p195100/', 17 | }, 18 | { 19 | brand: 'sony', 20 | model: 'ps5 console', 21 | series: 'sonyps5c', 22 | url: 'https://www.shopto.net/en/ps5hw01-playstation-5-console-p191472/', 23 | }, 24 | { 25 | brand: 'sony', 26 | model: 'ps5 digital', 27 | series: 'sonyps5de', 28 | url: 'https://www.shopto.net/en/ps5hw02-playstation-5-digital-console-p195341/', 29 | }, 30 | ], 31 | name: 'shopto', 32 | country: 'UK', 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/model/tesco-ie.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const TescoIE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: 'input.submit:nth-child(5)', 8 | text: ['add'], 9 | }, 10 | maxPrice: { 11 | container: '.linePriceAbbr', 12 | }, 13 | outOfStock: { 14 | container: '.noStockTxtCentered > strong:nth-child(1)', 15 | text: ['Sorry, this product is currently not available'], 16 | }, 17 | }, 18 | links: [ 19 | { 20 | brand: 'microsoft', 21 | model: 'xbox series x', 22 | series: 'xboxsx', 23 | url: 'https://secure.tesco.ie/groceries/Product/Details/?id=307835209', 24 | }, 25 | { 26 | brand: 'sony', 27 | model: 'ps5 digital', 28 | series: 'sonyps5de', 29 | url: 'https://www.tesco.ie/groceries/product/details/?id=307756010', 30 | }, 31 | ], 32 | name: 'tesco-ie', 33 | country: 'IE', 34 | }; 35 | -------------------------------------------------------------------------------- /src/store/model/argos-ie.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const ArgosIE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.btnbuyreserve', 8 | text: ['buy or reserve'], 9 | }, 10 | maxPrice: { 11 | container: '.price', 12 | }, 13 | outOfStock: { 14 | container: '#subCopy', 15 | text: ["We're working hard to get more stock."], 16 | }, 17 | }, 18 | links: [ 19 | { 20 | brand: 'microsoft', 21 | model: 'xbox series x', 22 | series: 'xboxsx', 23 | url: 'http://www.argos.ie/static/Product/partNumber/8448262/Trail/searchtext%3EXBOX+SERIES+X.htm', 24 | }, 25 | { 26 | brand: 'sony', 27 | model: 'ps5 digital', 28 | series: 'sonyps5de', 29 | url: 'http://www.argos.ie/static/Product/partNumber/8349000/Trail/searchtext%3EPS5+CONSOLE.htm', 30 | }, 31 | ], 32 | name: 'argos-ie', 33 | country: 'IE', 34 | }; 35 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ## Background 4 | 5 | Remember on September 17th, 2020 at 9 AM EST the Nvidia site went from **Notify Me** to **Out of Stock** instantly? Well, they didn't sell any cards. The real reason was that they weren't ready to sell them to us yet. That's right, they turned off their third party storefronts because they were being overloaded with our clicks. They still kept the other cards that use those APIs online, but they removed that one. It was re-enabled at some point for a brief moment, but the same thing happened -- servers overloaded with API requests. 6 | 7 | This is where streetmerchant comes in. It doesn't buy anything for you, but it makes it more of a stress free job to refresh and check sites while you go about your daily business. People took off work, missed appointments, and gave up other lively needs in hopes to buy a _graphics card_. Now we reach beyond graphics cards in hopes for other products! 8 | 9 | Please enjoy, 10 | 11 | jef 12 | -------------------------------------------------------------------------------- /terraform/dashboard.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "type": "metric", 5 | "x": 0, 6 | "y": 0, 7 | "width": 18, 8 | "height": 12, 9 | "properties": { 10 | "metrics": ${out_of_stock}, 11 | "view": "timeSeries", 12 | "stacked": false, 13 | "region": "${region}", 14 | "start": "-PT1H", 15 | "end": "P0D", 16 | "stat": "Sum", 17 | "period": 300, 18 | "title": "out of stock" 19 | } 20 | }, 21 | { 22 | "type": "metric", 23 | "x": 0, 24 | "y": 0, 25 | "width": 18, 26 | "height": 12, 27 | "properties": { 28 | "metrics": ${error}, 29 | "view": "timeSeries", 30 | "stacked": false, 31 | "region": "${region}", 32 | "start": "-PT1H", 33 | "end": "P0D", 34 | "stat": "Sum", 35 | "period": 300, 36 | "title": "error" 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | {% set title = config.site_name %} 5 | {% if page and page.meta and page.meta.title %} 6 | {% set title = title ~ " - " ~ page.meta.title %} 7 | {% elif page and page.title and not page.is_homepage %} 8 | {% set title = title ~ " - " ~ page.title | striptags %} 9 | {% endif %} 10 | 11 | {% set image = config.site_url ~ '/assets/images/streetmerchant-github.png' %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /src/messaging/pagerduty.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import PDClient from 'node-pagerduty'; 4 | import {config} from '../config'; 5 | 6 | const pd = new PDClient(''); 7 | 8 | export function sendPagerDutyNotification(link: Link, store: Store) { 9 | if (config.notifications.pagerduty.integrationKey) { 10 | logger.debug('↗ sending pagerduty message'); 11 | const links = [{href: link.url, text: 'Visit Store'}]; 12 | if (link.cartUrl) { 13 | links.push({ 14 | href: link.cartUrl, 15 | text: 'Add to Cart', 16 | }); 17 | } 18 | 19 | pd.events.sendEvent({ 20 | dedup_key: link.url, 21 | event_action: 'trigger', 22 | payload: { 23 | links, 24 | severity: config.notifications.pagerduty.severity, 25 | source: store.name, 26 | summary: Print.inStock(link, store), 27 | }, 28 | routing_key: config.notifications.pagerduty.integrationKey, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/messaging/captcha.ts: -------------------------------------------------------------------------------- 1 | import {config} from '../config'; 2 | import {sendDMAndGetResponseAsync as getWithDiscord} from './discord'; 3 | import {sendDMAndGetResponseAsync as getWithSlack} from './slack'; 4 | import {DMPayload} from '.'; 5 | 6 | export type CaptchaPayload = DMPayload; // for now this is a 1:1 alias 7 | 8 | const {service} = config.captchaHandler; 9 | 10 | /** 11 | * Picks the service that will handle the user interaction 12 | * based on configuration and sends the payload to that service 13 | * 14 | * @param payload the content to send to user 15 | * @param timeout timeout for response, in seconds 16 | * @returns response from user 17 | */ 18 | export async function getCaptchaInputAsync( 19 | payload: CaptchaPayload, 20 | timeout?: number 21 | ): Promise { 22 | switch (service) { 23 | case 'discord': 24 | return await getWithDiscord(payload, timeout); 25 | case 'slack': 26 | return await getWithSlack(payload, timeout); 27 | default: 28 | return ''; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/store/model/microsoft.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Microsoft: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: 'button[aria-label="Checkout bundle"]', 8 | text: ['Checkout'], 9 | }, 10 | outOfStock: { 11 | container: 'button[aria-label="Checkout bundle"]', 12 | text: ['Out of stock'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.xbox.com/en-us/configure/8WJ714N3RBTL', 21 | }, 22 | { 23 | brand: 'microsoft', 24 | model: 'xbox series x', 25 | series: 'xboxsx', 26 | url: 'https://www.xbox.com/en-us/configure/8WJ714N3RBTL', 27 | }, 28 | { 29 | brand: 'microsoft', 30 | model: 'xbox series s', 31 | series: 'xboxss', 32 | url: 'https://www.xbox.com/en-us/configure/942J774TP9JN', 33 | }, 34 | ], 35 | name: 'microsoft', 36 | country: 'US', 37 | }; 38 | -------------------------------------------------------------------------------- /src/store/model/microsoft-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const MicrosoftCA: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: 'button[aria-label="Checkout bundle"]', 8 | text: ['Checkout'], 9 | }, 10 | outOfStock: { 11 | container: 'button[aria-label="Checkout bundle"]', 12 | text: ['Out of stock'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.xbox.com/en-ca/configure/8WJ714N3RBTL', 21 | }, 22 | { 23 | brand: 'microsoft', 24 | model: 'xbox series x', 25 | series: 'xboxsx', 26 | url: 'https://www.xbox.com/en-ca/configure/8WJ714N3RBTL', 27 | }, 28 | { 29 | brand: 'microsoft', 30 | model: 'xbox series s', 31 | series: 'xboxss', 32 | url: 'https://www.xbox.com/en-ca/configure/942J774TP9JN', 33 | }, 34 | ], 35 | name: 'microsoft-ca', 36 | country: 'CA', 37 | }; 38 | -------------------------------------------------------------------------------- /src/store/model/gamestop-de.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const GamestopDE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: '#btnAddToCart', 9 | text: ['In den Warenkorb'], 10 | }, 11 | { 12 | container: '#btnAddToCart', 13 | text: ['Vorbestellen'], 14 | }, 15 | ], 16 | maxPrice: { 17 | container: '.buySection .prodPriceCont', 18 | euroFormat: true, 19 | }, 20 | outOfStock: { 21 | container: '.megaButton', 22 | text: ['Nicht verfügbar'], 23 | }, 24 | }, 25 | links: [ 26 | { 27 | brand: 'sony', 28 | model: 'ps5 console', 29 | series: 'sonyps5c', 30 | url: 'https://www.gamestop.de/PS5/Games/58665', 31 | }, 32 | { 33 | brand: 'sony', 34 | model: 'ps5 digital', 35 | series: 'sonyps5de', 36 | url: 'https://www.gamestop.de/PS5/Games/60315', 37 | }, 38 | ], 39 | name: 'gamestop-de', 40 | country: 'DE', 41 | successStatusCodes: [[0, 399], 404], 42 | }; 43 | -------------------------------------------------------------------------------- /src/store/model/lmc.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const LandmarkComputers: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: '.stock-info-message', 9 | text: ['In Stock', 'Low In Stock', 'Stock in warehouse'], 10 | }, 11 | maxPrice: { 12 | container: '.product-views-price-lead', 13 | euroFormat: false, 14 | }, 15 | outOfStock: { 16 | container: '.stock-info-message', 17 | text: ['Pre-order', 'Call for ETA'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'msi', 23 | model: 'gaming x trio', 24 | series: '3080', 25 | url: 'https://www.lmc.com.au/msi-geforce-rtx-3080-gaming-x-trio-10g-gaming-graphics-card', 26 | }, 27 | { 28 | brand: 'leadtek', 29 | model: 'hurricane', 30 | series: '3080', 31 | url: 'https://www.lmc.com.au/leadtek-geforce-rtx-3080-hurricane-12789000110-10g-gddr6x-hdmi2.1-3xdp1.4a', 32 | }, 33 | ], 34 | name: 'landmark-computers', 35 | country: 'AU', 36 | }; 37 | -------------------------------------------------------------------------------- /src/messaging/gotify.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {config} from '../config'; 4 | import fetch from 'node-fetch'; 5 | import {URLSearchParams} from 'url'; 6 | 7 | const {gotify} = config.notifications; 8 | 9 | export function sendGotifyNotification(link: Link, store: Store) { 10 | if (!gotify.token || !gotify.url) return; 11 | 12 | (async () => { 13 | const params = new URLSearchParams(); 14 | params.append('title', Print.inStock(link, store)); 15 | params.append('message', Print.productInStock(link)); 16 | params.append('priority', gotify.priority.toString()); 17 | const response = await fetch( 18 | `${gotify.url}/message?token=${gotify.token}`, 19 | { 20 | method: 'POST', 21 | body: params, 22 | } 23 | ); 24 | 25 | const json = await response.json(); 26 | 27 | if (json.error) { 28 | logger.error('✖ could not send gotify message', json.error); 29 | } else { 30 | logger.info('✔ gotify message sent'); 31 | } 32 | })(); 33 | } 34 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "credential_file" { 2 | type = string 3 | description = "your aws credentials file" 4 | default = "~/.aws/credentials" 5 | } 6 | 7 | variable "credential_profile" { 8 | type = string 9 | description = "the section in ~/.aws/credentials with your desired aws_access_key_id and aws_secret_access_key values" 10 | default = "default" 11 | } 12 | 13 | variable "region" { 14 | type = string 15 | description = "aws region" 16 | default = "eu-west-2" 17 | } 18 | 19 | variable "app_name" { 20 | type = string 21 | default = "streetmerchant" 22 | } 23 | 24 | variable "memory" { 25 | type = string 26 | default = "2048" 27 | description = "ecs task memory" 28 | } 29 | 30 | variable "cpu" { 31 | type = number 32 | default = 1024 33 | description = "ecs task cpu" 34 | } 35 | 36 | variable "streetmerchant_env" { 37 | type = map 38 | description = "name/value pairs for .env values" 39 | default = {} 40 | } 41 | 42 | variable "ecs_task_execution_role_name" { 43 | description = "ECS task execution role name" 44 | default = "myEcsTaskExecutionRole" 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jef LeCompte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /helpers/brandList.ts: -------------------------------------------------------------------------------- 1 | import {storeList} from '../src/store/model/index'; 2 | 3 | // First get the grouped and deduplicated data 4 | const groupedByBrandAndModel = [...storeList] 5 | .map(storeInfo => storeInfo[1]) 6 | .flatMap(store => store.links || []) 7 | .reduce((groupMap, link) => { 8 | const key = `${link.brand}`; 9 | if (!groupMap.has(key)) { 10 | groupMap.set(key, new Set()); 11 | } 12 | groupMap.get(key)!.add(link.model); 13 | return groupMap; 14 | }, new Map>()); 15 | 16 | // Convert to sorted table format 17 | const sortedEntries = [...groupedByBrandAndModel.entries()] 18 | .sort(([brandA], [brandB]) => brandA.localeCompare(brandB)) 19 | .map(([brand, modelSet]) => { 20 | const sortedModels = [...modelSet].sort().join('`, `'); 21 | return `| \`${brand}\` | \`${sortedModels}\` |`; 22 | }); 23 | 24 | // Create table header and combine with data 25 | const tableHeader = '| Brand | Model |'; 26 | const headerSeparator = '|:---:|---|'; 27 | const formattedTable = [tableHeader, headerSeparator, ...sortedEntries].join( 28 | '\n' 29 | ); 30 | 31 | console.log(formattedTable); 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | build_lint: 8 | name: Build and lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 16.18.0 17 | - name: Install Dependencies 18 | run: npm ci 19 | - name: Compile TypeScript 20 | run: npm run compile 21 | - name: Run linter 22 | run: npm run lint 23 | build_docker: 24 | name: Build Docker image 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v3 29 | - name: Run paths filter 30 | uses: dorny/paths-filter@v2 31 | id: filter 32 | with: 33 | filters: | 34 | build: 35 | - 'Dockerfile' 36 | - 'package*.json' 37 | - name: Build Docker image 38 | if: steps.filter.outputs.build == 'true' 39 | run: docker build . 40 | -------------------------------------------------------------------------------- /src/store/model/smythstoys-ie.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const SmythsToysIE: Store = { 4 | currency: '€', 5 | disableAdBlocker: true, 6 | labels: { 7 | inStock: { 8 | container: '#addToCartButton', 9 | text: ['add to basket'], 10 | }, 11 | maxPrice: { 12 | container: '.price_tag', 13 | euroFormat: false, 14 | }, 15 | outOfStock: { 16 | container: '.instoreMessage', 17 | text: ['out of stock'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'microsoft', 23 | model: 'xbox series x', 24 | series: 'xboxsx', 25 | url: 'https://www.smythstoys.com/ie/en-ie/video-games-and-tablets/xbox-gaming/xbox-series-x-%7c-s/xbox-series-x-%7c-s-consoles/xbox-series-x-1tb-console/p/192012', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 console', 30 | series: 'sonyps5c', 31 | url: 'https://www.smythstoys.com/ie/en-ie/video-games-and-tablets/playstation-5/playstation-5-consoles/playstation-5-console/p/191259', 32 | }, 33 | ], 34 | name: 'smythstoys-ie', 35 | country: 'IE', 36 | waitUntil: 'domcontentloaded', 37 | }; 38 | -------------------------------------------------------------------------------- /src/messaging/telegram.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {TelegramClient} from 'messaging-api-telegram'; 4 | import {config} from '../config'; 5 | 6 | const {telegram} = config.notifications; 7 | 8 | const client = new TelegramClient({ 9 | accessToken: telegram.accessToken, 10 | }); 11 | 12 | export function sendTelegramMessage(link: Link, store: Store) { 13 | if (telegram.accessToken && telegram.chatId) { 14 | logger.debug('↗ sending telegram message'); 15 | 16 | (async () => { 17 | const message = Print.productInStock(link); 18 | const results = []; 19 | 20 | for (const chatId of telegram.chatId) { 21 | try { 22 | results.push( 23 | client.sendMessage( 24 | chatId, 25 | `${Print.inStock(link, store)}\n${message}` 26 | ) 27 | ); 28 | logger.info('✔ telegram message sent'); 29 | } catch (error: unknown) { 30 | logger.error("✖ couldn't send telegram message", error); 31 | } 32 | } 33 | 34 | await Promise.all(results); 35 | })(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/store/model/corsair.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Corsair: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: '.add_to_cart_form', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '.product-price', 12 | euroFormat: false, 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.corsair.com/us/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020181-NA', 21 | }, 22 | { 23 | brand: 'corsair', 24 | model: '750 platinum', 25 | series: 'sf', 26 | url: 'https://www.corsair.com/us/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020186-NA', 27 | }, 28 | { 29 | brand: 'corsair', 30 | model: '600 platinum', 31 | series: 'sf', 32 | url: 'https://www.corsair.com/us/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020182-NA', 33 | }, 34 | ], 35 | name: 'corsair', 36 | country: 'US', 37 | }; 38 | -------------------------------------------------------------------------------- /src/store/model/antonline.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AntOnline: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: '.uk-button', 8 | text: ['Add to Cart'], 9 | }, 10 | maxPrice: { 11 | container: '.cPrice', 12 | euroFormat: false, 13 | }, 14 | outOfStock: { 15 | container: '.priceView-price .priceView-hero-price span', 16 | text: ['Sold Out'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'sony', 22 | model: 'ps5 digital', 23 | series: 'sonyps5de', 24 | url: 'https://www.antonline.com/Sony/Electronics/Gaming_Devices/Gaming_Consoles/1409507', 25 | }, 26 | { 27 | brand: 'microsoft', 28 | model: 'xbox series x', 29 | series: 'xboxsx', 30 | url: 'https://www.antonline.com/Microsoft/Electronics/Gaming_Devices/Gaming_Consoles/1414487', 31 | }, 32 | { 33 | brand: 'microsoft', 34 | model: 'xbox series s', 35 | series: 'xboxss', 36 | url: 'https://www.antonline.com/Microsoft/Electronics/Gaming_Devices/Gaming_Consoles/1409527', 37 | }, 38 | ], 39 | name: 'antonline', 40 | country: 'US', 41 | }; 42 | -------------------------------------------------------------------------------- /src/store/model/bpmpower.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const BpmPower: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.dispoSiProd >span', 8 | text: ['Disponibile'], 9 | }, 10 | maxPrice: { 11 | container: 'p.prezzoScheda:nth-child(1)', 12 | euroFormat: true, 13 | }, 14 | outOfStock: { 15 | container: '.dispoSiProd >span', 16 | text: ['Esaurito'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.bpm-power.com/it/online/audio/auricolari-audio/apple-airpods-2-2019-b1492931', 25 | }, 26 | { 27 | brand: 'msi', 28 | model: 'ventus 3x oc', 29 | series: '3070', 30 | url: 'https://www.bpm-power.com/it/online/componenti-pc/schede-video/nvidia-msi-rtx-3070-b1710142', 31 | }, 32 | { 33 | brand: 'amd', 34 | model: '5800x', 35 | series: 'ryzen5800', 36 | url: 'https://www.bpm-power.com/it/online/componenti-pc/processori/cpu-amd-ryzen-7-b1710075', 37 | }, 38 | ], 39 | name: 'bpm-power', 40 | country: 'IT', 41 | }; 42 | -------------------------------------------------------------------------------- /src/store/model/euronics.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Euronics: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.purchaseButtonsWidth', 8 | text: ['Aggiungi al carrello'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'sony', 14 | model: 'ps5 console', 15 | series: 'sonyps5c', 16 | url: 'https://www.euronics.it/console/sony-computer/playstation-5/eProd202008906', 17 | }, 18 | { 19 | brand: 'sony', 20 | model: 'ps5 digital', 21 | series: 'sonyps5de', 22 | url: 'https://www.euronics.it/console/sony-computer/playstation-5-digital-edition/eProd202008907', 23 | }, 24 | { 25 | brand: 'microsoft', 26 | model: 'xbox series x', 27 | series: 'xboxsx', 28 | url: 'https://www.euronics.it/console/microsoft/xbox-series-x-1tb-it-italy-sxto/eProd202008981', 29 | }, 30 | { 31 | brand: 'microsoft', 32 | model: 'xbox series s', 33 | series: 'xboxss', 34 | url: 'https://www.euronics.it/console/microsoft/xbox-series-s-512gb-it-italy-ltsn/eProd202008982', 35 | }, 36 | ], 37 | name: 'euronics', 38 | country: 'IT', 39 | }; 40 | -------------------------------------------------------------------------------- /src/messaging/twitter.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import Twitter from 'twitter'; 4 | import {config} from '../config'; 5 | 6 | const {twitter} = config.notifications; 7 | 8 | const client = new Twitter({ 9 | access_token_key: twitter.accessTokenKey, 10 | access_token_secret: twitter.accessTokenSecret, 11 | consumer_key: twitter.consumerKey, 12 | consumer_secret: twitter.consumerSecret, 13 | }); 14 | 15 | export function sendTweet(link: Link, store: Store) { 16 | if ( 17 | twitter.accessTokenKey && 18 | twitter.accessTokenSecret && 19 | twitter.consumerKey && 20 | twitter.consumerSecret 21 | ) { 22 | logger.debug('↗ sending twitter message'); 23 | 24 | let status = `${Print.inStock(link, store)}\n${ 25 | link.cartUrl ? link.cartUrl : link.url 26 | }`; 27 | 28 | if (twitter.tweetTags) { 29 | status += `\n\n${twitter.tweetTags}`; 30 | } 31 | 32 | client.post('statuses/update', {status}, error => { 33 | if (error) { 34 | logger.error("✖ couldn't send twitter notification", error); 35 | } else { 36 | logger.info('✔ twitter notification sent'); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | The world's easiest, most powerful stock checker 6 |

7 |

8 | To get started, visit jef.buzz/streetmerchant 9 |

10 | 11 | ## Features 12 | 13 | First and foremost, this service _will not_ automatically buy for you. 14 | 15 | - **Checks stock continuously** -- runs 24/7, 365, looking for the items you want. 16 | - **Ready for checkout** -- ability to add to cart when available and even opens the browser for you. 17 | - **Notifications galore** -- when you're not by your computer, worry free with notifications to most platforms and devices when an item comes in stock. 18 | 19 | ## Quick start 20 | 21 | streetmerchant runs on Node.js: 22 | 23 | ```shell 24 | git clone https://github.com/jef/streetmerchant.git 25 | cd streetmerchant && npm i && npm run start 26 | ``` 27 | 28 | For more information and customization, visit [jef.buzz/streetmerchant/getting-started](https://jef.buzz/streetmerchant/getting-started). 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.18.0-alpine3.16 AS builder 2 | 3 | LABEL org.opencontainers.image.source="https://github.com/jef/streetmerchant" 4 | LABEL org.opencontainers.image.description="The world's easiest, most powerful stock checker" 5 | LABEL org.opencontainers.image.licenses="MIT" 6 | 7 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 8 | 9 | WORKDIR /build 10 | 11 | COPY package.json package.json 12 | COPY package-lock.json package-lock.json 13 | COPY tsconfig.json tsconfig.json 14 | RUN npm ci 15 | 16 | COPY src/ src/ 17 | COPY test/ test/ 18 | RUN npm run compile 19 | RUN npm prune --production 20 | 21 | FROM node:16.18.0-alpine3.16 22 | 23 | RUN apk add --no-cache chromium 24 | 25 | ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \ 26 | DOCKER=true 27 | 28 | RUN addgroup -S appuser && adduser -S -g appuser appuser \ 29 | && mkdir -p /home/appuser/Downloads /app \ 30 | && chown -R appuser:appuser /home/appuser \ 31 | && chown -R appuser:appuser /app 32 | 33 | USER appuser 34 | 35 | WORKDIR /app 36 | 37 | COPY --from=builder /build/node_modules/ node_modules/ 38 | COPY --from=builder /build/build/ build/ 39 | COPY web/ web/ 40 | COPY package.json package.json 41 | 42 | ENTRYPOINT ["npm", "run"] 43 | CMD ["start:production"] 44 | -------------------------------------------------------------------------------- /src/messaging/sound.ts: -------------------------------------------------------------------------------- 1 | import playerLib, {PlaySound} from 'play-sound'; 2 | import {config} from '../config'; 3 | import fs from 'fs'; 4 | import {logger} from '../logger'; 5 | 6 | let player: PlaySound; 7 | 8 | if (config.notifications.playSound) { 9 | player = config.notifications.soundPlayer 10 | ? playerLib({players: [config.notifications.soundPlayer]}) 11 | : playerLib(); 12 | 13 | if (player.player === null) { 14 | logger.warn("✖ couldn't find sound player"); 15 | } else { 16 | const playerName = player.player; 17 | logger.info(`✔ sound player found: ${playerName}`); 18 | } 19 | } 20 | 21 | export function playSound() { 22 | if (config.notifications.playSound && player.player !== null) { 23 | logger.debug('↗ playing sound'); 24 | 25 | fs.access(config.notifications.playSound, fs.constants.F_OK, error => { 26 | if (error) { 27 | logger.error(`✖ error opening sound file: ${error.message}`); 28 | return; 29 | } 30 | 31 | player.play(config.notifications.playSound, (error: Error) => { 32 | if (error) { 33 | logger.error("✖ couldn't play sound", error); 34 | } 35 | 36 | logger.info('✔ played sound'); 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/store/model/comet.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Comet: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.caption', 8 | text: ['Aggiungi al carrello'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'sony', 14 | cartUrl: 'https://www.comet.it/cart/insert/PSX01802A/online', 15 | model: 'ps5 console', 16 | series: 'sonyps5c', 17 | url: 'https://www.comet.it/ps5/sony-playstation-5', 18 | }, 19 | { 20 | brand: 'sony', 21 | model: 'ps5 digital', 22 | series: 'sonyps5de', 23 | url: 'https://www.comet.it/ps5/sony-playstation-5-digital-edition', 24 | }, 25 | { 26 | brand: 'microsoft', 27 | cartUrl: 'https://www.comet.it/cart/insert/MIS01077A/online', 28 | model: 'xbox series x', 29 | series: 'xboxsx', 30 | url: 'https://www.comet.it/xbox-serie-x/xbox-series-x', 31 | }, 32 | { 33 | brand: 'microsoft', 34 | cartUrl: 'https://www.comet.it/cart/insert/MIS010761/online', 35 | model: 'xbox series s', 36 | series: 'xboxss', 37 | url: 'https://www.comet.it/xbox-serie-x/xbox-series-s', 38 | }, 39 | ], 40 | name: 'comet', 41 | country: 'IT', 42 | }; 43 | -------------------------------------------------------------------------------- /src/store/model/thewarehouse.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const TheWarehouse: Store = { 4 | currency: '$', 5 | labels: { 6 | outOfStock: { 7 | container: 8 | '#maincontent > div.container.product-detail.product-wrapper.pb-xl.pb-lg-xxxl > div > div:nth-child(2) > div.col-12.col-md-6.col-lg-5 > div > div.product-buying-box > div.cart-and-ipay.mt-md > div > div > button:disabled', 9 | text: ['Add to Cart'], 10 | }, 11 | }, 12 | links: [ 13 | { 14 | brand: 'sony', 15 | model: 'ps5 console', 16 | series: 'sonyps5c', 17 | url: 'https://www.thewarehouse.co.nz/R2695122.html', 18 | }, 19 | { 20 | brand: 'sony', 21 | model: 'ps5 digital', 22 | series: 'sonyps5de', 23 | url: 'https://www.thewarehouse.co.nz/R2695123.html', 24 | }, 25 | { 26 | brand: 'microsoft', 27 | model: 'xbox series x', 28 | series: 'xboxsx', 29 | url: 'https://www.thewarehouse.co.nz/R2708605.html', 30 | }, 31 | { 32 | brand: 'microsoft', 33 | model: 'xbox series s', 34 | series: 'xboxss', 35 | url: 'https://www.thewarehouse.co.nz/R2708604.html', 36 | }, 37 | ], 38 | name: 'thewarehouse', 39 | country: 'NZ', 40 | }; 41 | -------------------------------------------------------------------------------- /src/store/model/unieuro.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Unieuro: Store = { 4 | currency: '€', 5 | labels: { 6 | captcha: { 7 | container: 'body', 8 | text: ['Too Many Requests.'], 9 | }, 10 | inStock: { 11 | container: '.price-container', 12 | text: ['Aggiungi al carrello'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'sony', 18 | model: 'ps5 console', 19 | series: 'sonyps5c', 20 | url: 'https://www.unieuro.it/online/Playstation-5/PlayStation-5-pidSONPS5DISC', 21 | }, 22 | { 23 | brand: 'sony', 24 | model: 'ps5 digital', 25 | series: 'sonyps5de', 26 | url: 'https://www.unieuro.it/online/Playstation-5/PlayStation-5-Digital-Edition-pidSONPS5DIGITAL', 27 | }, 28 | { 29 | brand: 'microsoft', 30 | model: 'xbox series x', 31 | series: 'xboxsx', 32 | url: 'https://www.unieuro.it/online/Xbox-Series/Xbox-Series-X-pidDBLRRT00008', 33 | }, 34 | { 35 | brand: 'microsoft', 36 | model: 'xbox series s', 37 | series: 'xboxss', 38 | url: 'https://www.unieuro.it/online/Xbox-Series/Xbox-Series-S-pidDBLRRS00008', 39 | }, 40 | ], 41 | name: 'unieuro', 42 | country: 'IT', 43 | }; 44 | -------------------------------------------------------------------------------- /src/store/model/noelleeming.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NoelLeeming: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: 9 | '#bd > div.product__hero > div > div.columns.product__info.medium-6.small-12 > div.product__price-cart.mbs.row > div:nth-child(3) > div > div.product__cta-buttons.columns.small-12 > form > button > strong', 10 | text: ['Buy Now'], 11 | }, 12 | ], 13 | }, 14 | links: [ 15 | { 16 | brand: 'sony', 17 | model: 'ps5 console', 18 | series: 'sonyps5c', 19 | url: 'https://www.noelleeming.co.nz/prod192865.html', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 digital', 24 | series: 'sonyps5de', 25 | url: 'https://www.noelleeming.co.nz/prod192879.html', 26 | }, 27 | { 28 | brand: 'microsoft', 29 | model: 'xbox series x', 30 | series: 'xboxsx', 31 | url: 'https://www.noelleeming.co.nz/prod193770.html', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series s', 36 | series: 'xboxss', 37 | url: 'https://www.noelleeming.co.nz/prod200475.html', 38 | }, 39 | ], 40 | name: 'noelleeming', 41 | country: 'NZ', 42 | }; 43 | -------------------------------------------------------------------------------- /src/store/model/medimax.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Medimax: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: '.product-cart-add-to-cart-button', 9 | text: ['In den Warenkorb'], 10 | }, 11 | { 12 | container: '.stock-message', 13 | text: ['Lieferung in'], 14 | }, 15 | ], 16 | maxPrice: { 17 | container: '.priceOfProduct', 18 | euroFormat: true, 19 | }, 20 | outOfStock: { 21 | container: '.content .large', 22 | text: ['Ihr MEDIMAX Team'], 23 | }, 24 | }, 25 | links: [ 26 | { 27 | brand: 'test:brand', 28 | model: 'test:model', 29 | series: 'test:series', 30 | url: 'https://www.medimax.de/p/1311642/drivesmart-51-lmt-d-ce', 31 | }, 32 | { 33 | brand: 'sony', 34 | model: 'ps5 console', 35 | series: 'sonyps5c', 36 | url: 'https://www.medimax.de/p/1315336/play-station-5-825gb-ssd', 37 | }, 38 | { 39 | brand: 'sony', 40 | model: 'ps5 digital', 41 | series: 'sonyps5de', 42 | url: 'https://www.medimax.de/p/1315337/play-station-5-digital-edition-825gb-ssd', 43 | }, 44 | ], 45 | name: 'medimax', 46 | country: 'DE', 47 | }; 48 | -------------------------------------------------------------------------------- /src/store/model/argos.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Argos: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: 'button[data-test="add-to-trolley-button-button"', 8 | text: ['to trolley'], 9 | }, 10 | maxPrice: { 11 | container: 'li[itemprop="price"]', 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.argos.co.uk/product/5718469', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.argos.co.uk/product/8349000', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.argos.co.uk/product/8349024', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.argos.co.uk/product/8448262', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.argos.co.uk/product/8448248', 44 | }, 45 | ], 46 | name: 'argos', 47 | country: 'UK', 48 | }; 49 | -------------------------------------------------------------------------------- /src/store/model/mightyape.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const MightyApe: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: 'div.status', 9 | text: [' In stock at '], 10 | }, 11 | ], 12 | outOfStock: [ 13 | { 14 | container: 'div.status', 15 | text: [' Unavailable '], 16 | }, 17 | ], 18 | }, 19 | links: [ 20 | { 21 | brand: 'sony', 22 | model: 'ps5 console', 23 | series: 'sonyps5c', 24 | url: 'https://www.mightyape.co.nz/product/sony-playstation-5-console/31675007', 25 | }, 26 | { 27 | brand: 'sony', 28 | model: 'ps5 digital', 29 | series: 'sonyps5de', 30 | url: 'https://www.mightyape.co.nz/product/sony-playstation-5-digital-edition-console/33505481', 31 | }, 32 | { 33 | brand: 'microsoft', 34 | model: 'xbox series x', 35 | series: 'xboxsx', 36 | url: 'https://www.mightyape.co.nz/product/xbox-series-x-console/30472387', 37 | }, 38 | { 39 | brand: 'microsoft', 40 | model: 'xbox series s', 41 | series: 'xboxss', 42 | url: 'https://www.mightyape.co.nz/product/xbox-series-s-all-digital-console/33856647', 43 | }, 44 | ], 45 | name: 'mightyape', 46 | country: 'NZ', 47 | }; 48 | -------------------------------------------------------------------------------- /src/messaging/twilio.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {Twilio} from 'twilio'; 4 | import {config} from '../config'; 5 | 6 | const {twilio} = config.notifications; 7 | let client: Twilio; 8 | 9 | if (twilio.accountSid && twilio.authToken) { 10 | client = new Twilio(twilio.accountSid, twilio.authToken); 11 | } 12 | 13 | export function sendTwilioMessage(link: Link, store: Store) { 14 | if (client) { 15 | logger.debug('↗ sending twilio message'); 16 | 17 | (async () => { 18 | const givenUrl = 19 | link.cartUrl && config.store.autoAddToCart ? link.cartUrl : link.url; 20 | const message = `${Print.inStock(link, store)}\n${givenUrl}`; 21 | const numbers = twilio.to.split(','); 22 | const results = []; 23 | for (const number of numbers) { 24 | try { 25 | results.push( 26 | client.messages.create({ 27 | body: message, 28 | from: twilio.from, 29 | to: number, 30 | }) 31 | ); 32 | logger.info('✔ twilio message sent'); 33 | } catch (error: unknown) { 34 | logger.error("✖ couldn't send twilio message", error); 35 | } 36 | } 37 | 38 | await Promise.all(results); 39 | })(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/store/model/smythstoys.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const SmythsToys: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '#addToCartButton', 8 | text: ['add to basket'], 9 | }, 10 | maxPrice: { 11 | container: '.price_tag', 12 | euroFormat: false, 13 | }, 14 | outOfStock: { 15 | container: '.instoreMessage', 16 | text: ['out of stock'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.smythstoys.com/uk/en-gb/video-games-and-tablets/video-games/call-of-duty-video-games/call-of-duty-black-ops-cold-war/call-of-duty-black-ops-cold-war-ps5/p/191951', 25 | }, 26 | { 27 | brand: 'sony', 28 | model: 'ps5 console', 29 | series: 'sonyps5c', 30 | url: 'https://www.smythstoys.com/uk/en-gb/video-games-and-tablets/playstation-5/playstation-5-consoles/playstation-5-console/p/191259', 31 | }, 32 | { 33 | brand: 'sony', 34 | model: 'ps5 digital', 35 | series: 'sonyps5de', 36 | url: 'https://www.smythstoys.com/uk/en-gb/video-games-and-tablets/playstation-5/playstation-5-consoles/playstation-5-digital-edition-console/p/191430', 37 | }, 38 | ], 39 | name: 'smythstoys', 40 | country: 'UK', 41 | }; 42 | -------------------------------------------------------------------------------- /src/store/model/officedepot.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const OfficeDepot: Store = { 4 | currency: '$', 5 | labels: { 6 | captcha: { 7 | container: 'body', 8 | text: ['please verify you are a human'], 9 | }, 10 | inStock: { 11 | container: '#productPurchase', 12 | text: ['add to cart'], 13 | }, 14 | maxPrice: { 15 | container: 'span[class^="price_column right"]', 16 | euroFormat: false, 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.officedepot.com/a/products/4652239/EVGA-GeForce-RTX-2060-Graphic-Card/', 25 | }, 26 | { 27 | brand: 'pny', 28 | model: 'xlr8 revel', 29 | series: '3080', 30 | url: 'https://www.officedepot.com/a/products/7189374/PNY-GeForce-RTX-3080-10GB-GDDR6X/', 31 | }, 32 | { 33 | brand: 'pny', 34 | model: 'xlr8 revel', 35 | series: '3080', 36 | url: 'https://www.officedepot.com/a/products/7791294/PNY-GeForce-RTX-3080-10GB-GDDR6X/', 37 | }, 38 | { 39 | brand: 'pny', 40 | model: 'dual fan', 41 | series: '3070', 42 | url: 'https://www.officedepot.com/a/products/1992758/PNY-GeForce-RTX-3070-8GB-GDDR6/', 43 | }, 44 | ], 45 | name: 'officedepot', 46 | country: 'US', 47 | }; 48 | -------------------------------------------------------------------------------- /src/store/model/gamestop-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const GamestopCA: Store = { 4 | currency: '$', 5 | labels: { 6 | maxPrice: { 7 | container: '.singleVariantText .prodPriceCont', 8 | }, 9 | outOfStock: { 10 | container: '#btnAddToCart[style*="display:none;"] ', 11 | text: ['add to cart'], 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.gamestop.ca/Switch/Games/727918/mario-kart-8-deluxe', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.gamestop.ca/PS5/Games/877522', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.gamestop.ca/PS5/Games/877523', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.gamestop.ca/Xbox%20Series%20X/Games/877779/xbox-series-x', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.gamestop.ca/Xbox%20Series%20X/Games/877780/xbox-series-s', 44 | }, 45 | ], 46 | name: 'gamestop-ca', 47 | country: 'CA', 48 | }; 49 | -------------------------------------------------------------------------------- /src/store/model/toysrus.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const ToysRUs: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: 'li.b-product_status', 8 | text: ['in stock'], 9 | }, 10 | maxPrice: { 11 | container: '.b-price-value', 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.toysrus.ca/en/Hasbro-Gaming---Operation-Game---styles-may-vary/99D52A22.html', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.toysrus.ca/en/PlayStation-5-Console/C443A89B.html', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.toysrus.ca/en/PlayStation-5-Digital-Edition/E4A019FE.html', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.toysrus.ca/en/XBOX-Series-X-Console/84D9A92D.html', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.toysrus.ca/en/XBOX-Series-S-Console/A43E2AF7.html', 44 | }, 45 | ], 46 | name: 'toysrus', 47 | country: 'US', 48 | }; 49 | -------------------------------------------------------------------------------- /src/messaging/apns.ts: -------------------------------------------------------------------------------- 1 | import * as apn from '@parse/node-apn'; 2 | import {config} from '../config'; 3 | import {logger} from '../logger'; 4 | import {Link, Store} from '../store/model'; 5 | 6 | const {apns} = config.notifications; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | export function sendApns(link: Link, store: Store) { 10 | const options = { 11 | token: { 12 | key: apns.apnsAuthKey, 13 | keyId: apns.apnsKeyId, 14 | teamId: apns.apnsTeamId, 15 | }, 16 | production: apns.apnsProduction, 17 | }; 18 | 19 | if ( 20 | options.token.key.length <= 0 || 21 | options.token.keyId.length <= 0 || 22 | options.token.teamId.length <= 0 23 | ) { 24 | return; 25 | } 26 | 27 | const apnProvider = new apn.Provider(options); 28 | 29 | const note = new apn.Notification(); 30 | 31 | note.badge = 1; 32 | note.sound = 'ping.aiff'; 33 | note.alert = '\uD83D\uDCE7 \u2709 You have a new message'; 34 | note.payload = {label: '1'}; 35 | note.topic = apns.apnsBundleId; 36 | 37 | apnProvider.send(note, apns.apnsDeviceToken).then(result => { 38 | // see documentation for an explanation of result 39 | if (result.sent) { 40 | logger.info('✔ push notification sent'); 41 | } else { 42 | logger.error("✖ couldn't send push notification", result.failed); 43 | } 44 | apnProvider.shutdown(); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Formalize a feature request from GitHub Discussions 3 | title: "[Feature]: " 4 | labels: 5 | - 'type: feature' 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Proposed solution 10 | description: What is solution to this feature request? 11 | placeholder: Solution description. 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Objective 17 | description: Link to discussion. 18 | placeholder: https://github.com/jef/streetmerchant/discussions 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Goals 24 | description: What is the purpose of feature request? 25 | placeholder: Add all relevant goals. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Non-goals 31 | description: What else could be accomplished with this feature request, but is currently out of scope? 32 | placeholder: Add all relevant non-goals. 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Anti-goals 38 | description: What could go wrong (side effects) if we implement this feature request? 39 | placeholder: Add all relevant anti-goals. 40 | validations: 41 | required: true 42 | -------------------------------------------------------------------------------- /src/messaging/freemobile.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {config} from '../config'; 4 | import {URL} from 'url'; 5 | import fetch from 'node-fetch'; 6 | 7 | const {freemobile} = config.notifications; 8 | 9 | const url = new URL('https://smsapi.free-mobile.fr/sendmsg'); 10 | 11 | if (freemobile.id && freemobile.apiKey) { 12 | url.searchParams.append('user', freemobile.id); 13 | url.searchParams.append('pass', freemobile.apiKey); 14 | } 15 | 16 | export function sendFreeMobileAlert(link: Link, store: Store) { 17 | if (freemobile.id && freemobile.apiKey) { 18 | logger.debug('↗ sending free mobile alert'); 19 | 20 | (async () => { 21 | const color = false; 22 | const sms = true; 23 | 24 | const message = `${Print.inStock( 25 | link, 26 | store, 27 | color, 28 | sms 29 | )}\n${Print.productInStock(link)}`; 30 | 31 | url.searchParams.append('msg', message); 32 | 33 | try { 34 | const response = await fetch(url.toString(), {method: 'GET'}); 35 | 36 | if (!response.ok) { 37 | logger.error("✖ couldn't send free mobile alert", response); 38 | return; 39 | } 40 | 41 | logger.info('✔ free mobile alert sent'); 42 | } catch (error: unknown) { 43 | logger.error("✖ couldn't send free mobile alert", error); 44 | } 45 | })(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/store/model/corsair-uk.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const CorsairUK: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '#addToCartForm', 8 | text: ['add to cart'], 9 | }, 10 | outOfStock: { 11 | container: '#addToCartForm', 12 | text: ['notify me when in stock'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.corsair.com/uk/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020181-UK', 21 | }, 22 | { 23 | brand: 'corsair', 24 | model: '750 platinum', 25 | series: 'sf', 26 | url: 'https://www.corsair.com/uk/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020186-UK', 27 | }, 28 | { 29 | brand: 'corsair', 30 | model: '600 platinum', 31 | series: 'sf', 32 | url: 'https://www.corsair.com/uk/en/Categories/Products/Power-Supply-Units/Power-Supply-Units-Advanced/SF-Series/p/CP-9020182-UK', 33 | }, 34 | { 35 | brand: 'corsair', 36 | model: '600 gold', 37 | series: 'sf', 38 | url: 'https://www.corsair.com/uk/en/Categories/Products/Power-Supply-Units/SF-Series%E2%84%A2-80-PLUS-Gold-Power-Supplies/p/CP-9020105-UK', 39 | }, 40 | ], 41 | name: 'corsair-uk', 42 | country: 'UK', 43 | }; 44 | -------------------------------------------------------------------------------- /src/store/model/game.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Game: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '.buyingOptions', 8 | text: ['Pre-order Now', 'Buy New'], 9 | }, 10 | maxPrice: { 11 | container: '.buyingOptions .btnPrice', 12 | euroFormat: false, 13 | }, 14 | outOfStock: { 15 | container: '.buyingOptions', 16 | text: ['out of stock'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.game.co.uk/en/ea-sports-fifa-21-500gb-ps4-bundle-2832947', 25 | }, 26 | { 27 | brand: 'sony', 28 | model: 'ps5 console', 29 | series: 'sonyps5c', 30 | url: 'https://www.game.co.uk/en/playstation-5-console-2826338', 31 | }, 32 | { 33 | brand: 'sony', 34 | model: 'ps5 digital', 35 | series: 'sonyps5de', 36 | url: 'https://www.game.co.uk/en/playstation-5-digital-edition-2826341', 37 | }, 38 | { 39 | brand: 'microsoft', 40 | model: 'xbox series x', 41 | series: 'xboxsx', 42 | url: 'https://www.game.co.uk/en/xbox-series-x-2831406', 43 | }, 44 | { 45 | brand: 'microsoft', 46 | model: 'xbox series s', 47 | series: 'xboxss', 48 | url: 'https://www.game.co.uk/en/xbox-series-x-2831406', 49 | }, 50 | ], 51 | name: 'game', 52 | country: 'UK', 53 | }; 54 | -------------------------------------------------------------------------------- /src/store/model/spielegrotte.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Spielegrotte: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: 9 | 'html > body > table > tbody > tr > td > div > table > tbody > tr > td > center > table > tbody > tr > td > a.klein > img', 10 | text: [''], 11 | }, 12 | ], 13 | maxPrice: { 14 | container: 15 | 'html > body > table > tbody > tr > td > div > table > tbody > tr > td > center > table > tbody > tr > td > font > b', 16 | euroFormat: true, 17 | }, 18 | outOfStock: { 19 | container: 20 | 'html > body > table > tbody > tr > td > div > table > tbody > tr > td > center > font > b', 21 | text: ['Dieses Produkt ist leider neu nicht mehr verfügbar'], 22 | }, 23 | }, 24 | links: [ 25 | { 26 | brand: 'test:brand', 27 | model: 'test:model', 28 | series: 'test:series', 29 | url: 'https://www.spielegrotte.de/index.php?kat=100056&anr=54288', 30 | }, 31 | { 32 | brand: 'sony', 33 | model: 'ps5 console', 34 | series: 'sonyps5c', 35 | url: 'https://www.spielegrotte.de/index.php?kat=100100&anr=56005', 36 | }, 37 | { 38 | brand: 'sony', 39 | model: 'ps5 digital', 40 | series: 'sonyps5de', 41 | url: 'https://www.spielegrotte.de/index.php?kat=100100&anr=56006', 42 | }, 43 | ], 44 | name: 'spielegrotte', 45 | country: 'DE', 46 | }; 47 | -------------------------------------------------------------------------------- /src/banner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const banner = { 4 | asciiVersion: ` 5 | ██████ ▄▄▄█████▓ ██▀███ ▓█████ ▓█████▄▄▄█████▓ ███▄ ▄███▓▓█████ ██▀███ ▄████▄ ██░ ██ ▄▄▄ ███▄ █ ▄▄▄█████▓ 6 | ▒██ ▒ ▓ ██▒ ▓▒▓██ ▒ ██▒▓█ ▀ ▓█ ▀▓ ██▒ ▓▒▓██▒▀█▀ ██▒▓█ ▀ ▓██ ▒ ██▒▒██▀ ▀█ ▓██░ ██▒▒████▄ ██ ▀█ █ ▓ ██▒ ▓▒ 7 | ░ ▓██▄ ▒ ▓██░ ▒░▓██ ░▄█ ▒▒███ ▒███ ▒ ▓██░ ▒░▓██ ▓██░▒███ ▓██ ░▄█ ▒▒▓█ ▄ ▒██▀▀██░▒██ ▀█▄ ▓██ ▀█ ██▒▒ ▓██░ ▒░ 8 | ▒ ██▒░ ▓██▓ ░ ▒██▀▀█▄ ▒▓█ ▄ ▒▓█ ▄░ ▓██▓ ░ ▒██ ▒██ ▒▓█ ▄ ▒██▀▀█▄ ▒▓▓▄ ▄██▒░▓█ ░██ ░██▄▄▄▄██ ▓██▒ ▐▌██▒░ ▓██▓ ░ 9 | ▒██████▒▒ ▒██▒ ░ ░██▓ ▒██▒░▒████▒░▒████▒ ▒██▒ ░ ▒██▒ ░██▒░▒████▒░██▓ ▒██▒▒ ▓███▀ ░░▓█▒░██▓ ▓█ ▓██▒▒██░ ▓██░ ▒██▒ ░ 10 | ▒ ▒▓▒ ▒ ░ ▒ ░░ ░ ▒▓ ░▒▓░░░ ▒░ ░░░ ▒░ ░ ▒ ░░ ░ ▒░ ░ ░░░ ▒░ ░░ ▒▓ ░▒▓░░ ░▒ ▒ ░ ▒ ░░▒░▒ ▒▒ ▓▒█░░ ▒░ ▒ ▒ ▒ ░░ 11 | ░ ░▒ ░ ░ ░ ░▒ ░ ▒░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░▒░ ░ ▒ ▒▒ ░░ ░░ ░ ▒░ ░ 12 | ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ 13 | ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 14 | ${process.env.npm_package_version}`, 15 | render(ascii: boolean, hexColor: string) { 16 | return chalk 17 | .hex(hexColor) 18 | .bold(ascii ? this.asciiVersion : this.stringVersion); 19 | }, 20 | stringVersion: `STREETMERCHANT 21 | ${process.env.npm_package_version}`, 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/pushover-notifications.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pushover-notifications' { 2 | export type PushoverCallback = 3 | | ((error: Error) => void) 4 | | ((error?: null, response: any) => void); 5 | 6 | export type Sound = 7 | | 'pushover' 8 | | 'bike' 9 | | 'bugle' 10 | | 'cashregister' 11 | | 'classical' 12 | | 'cosmic' 13 | | 'falling' 14 | | 'gamelan' 15 | | 'incoming' 16 | | 'intermission' 17 | | 'magic' 18 | | 'mechanical' 19 | | 'pianobar' 20 | | 'siren' 21 | | 'spacealarm' 22 | | 'tugboat' 23 | | 'alien' 24 | | 'climb' 25 | | 'persistent' 26 | | 'echo' 27 | | 'updown' 28 | | 'vibrate' 29 | | 'none'; 30 | 31 | export interface PushoverOptions { 32 | token: string; 33 | user: string; 34 | httpOptions?: {proxy: string}; 35 | onerror?: (error: Error | string) => void; 36 | update_sounds?: boolean; 37 | } 38 | 39 | export interface PushoverMessage { 40 | device?: string; 41 | expire?: number; 42 | file?: string | {name: string; data: string}; 43 | message: string; 44 | priority?: number; 45 | retry?: number; 46 | sound?: string; 47 | timestamp?: number; 48 | title?: string; 49 | url?: string; 50 | url_title?: string; 51 | } 52 | 53 | export class Pushover { 54 | constructor(options: PushoverOptions); 55 | send(message: PushoverMessage, callback: PushoverCallback); 56 | } 57 | 58 | export default Pushover; 59 | } 60 | -------------------------------------------------------------------------------- /src/messaging/redis.ts: -------------------------------------------------------------------------------- 1 | import redis, {RedisClient} from 'redis'; 2 | import {config} from '../config'; 3 | import {logger} from '../logger'; 4 | import {Link, Store} from '../store/model'; 5 | 6 | const {url} = config.notifications.redis; 7 | let client: RedisClient; 8 | 9 | function initRedis(): RedisClient | null { 10 | if (url) { 11 | client = redis.createClient({url}); 12 | } 13 | 14 | return null; 15 | } 16 | 17 | export function updateRedis(link: Link, store: Store) { 18 | try { 19 | if (client) { 20 | const key = `${store.name}:${link.brand}:${link.model}` 21 | .split(' ') 22 | .join('-'); 23 | 24 | const value = { 25 | ...link, 26 | labels: store.labels, 27 | links: store.links, 28 | name: store.name, 29 | updatedAt: new Date().toUTCString(), 30 | }; 31 | 32 | const message = JSON.stringify(value); 33 | client.set(key, message, error => { 34 | if (error) { 35 | logger.error(`✖ couldn't update redis for key (${key})`); 36 | } else { 37 | logger.info('✔ redis updated'); 38 | } 39 | }); 40 | 41 | client.publish('streetmerchant', message, error => { 42 | if (error) { 43 | logger.error("✖ couldn't publish to redis"); 44 | } else { 45 | logger.info('✔ redis message published'); 46 | } 47 | }); 48 | } 49 | } catch (error: unknown) { 50 | logger.error("✖ couldn't update redis", error); 51 | } 52 | } 53 | 54 | initRedis(); 55 | -------------------------------------------------------------------------------- /src/messaging/streamlabs.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {config} from '../config'; 4 | import {URLSearchParams} from 'url'; 5 | import fetch from 'node-fetch'; 6 | 7 | const {streamlabs} = config.notifications; 8 | let requestParams: URLSearchParams; 9 | 10 | if (streamlabs.accessToken && streamlabs.type) { 11 | requestParams = new URLSearchParams(); 12 | requestParams.append('access_token', streamlabs.accessToken); 13 | requestParams.append('type', streamlabs.type); 14 | requestParams.append('image_href', streamlabs.imageHref); 15 | requestParams.append('sound_href', streamlabs.soundHref); 16 | requestParams.append('duration', streamlabs.duration.toString()); 17 | } 18 | 19 | export function sendStreamLabsAlert(link: Link, store: Store) { 20 | if (requestParams) { 21 | logger.debug('↗ sending StreamLabs alert'); 22 | 23 | (async () => { 24 | const message = `${Print.inStock(link, store)}`; 25 | requestParams.set('message', message); 26 | 27 | try { 28 | const response = await fetch('https://streamlabs.com/api/v1.0/alerts', { 29 | method: 'POST', 30 | body: requestParams, 31 | }); 32 | 33 | const json = await response.json(); 34 | if (!json.success) throw Error(JSON.stringify(json)); 35 | 36 | logger.info('✔ StreamLabs alert sent'); 37 | } catch (error: unknown) { 38 | logger.error("✖ couldn't send StreamLabs alert", error); 39 | } 40 | })(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /terraform/resource-logging.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "main" { 2 | name = var.app_name 3 | retention_in_days = 3 4 | } 5 | 6 | locals { 7 | stores = split(",",var.streetmerchant_env["STORES"]) 8 | metrics = { 9 | out_of_stock = [for store in local.stores : ["${var.app_name}-out-of-stock", store]] 10 | error = [for store in local.stores : ["${var.app_name}-error", store]] 11 | } 12 | } 13 | 14 | resource "aws_cloudwatch_log_metric_filter" "out_of_stock" { 15 | for_each = toset(local.stores) 16 | 17 | log_group_name = aws_cloudwatch_log_group.main.name 18 | name = "${each.key}-out-of-stock" 19 | 20 | pattern = "${each.key} \"OUT OF STOCK\"" 21 | metric_transformation { 22 | name = each.key 23 | namespace = "${var.app_name}-out-of-stock" 24 | value = 1 25 | default_value = 0 26 | } 27 | } 28 | 29 | resource "aws_cloudwatch_log_metric_filter" "error" { 30 | for_each = toset(local.stores) 31 | 32 | log_group_name = aws_cloudwatch_log_group.main.name 33 | name = "${each.key}-error" 34 | 35 | pattern = "${each.key} \"ERROR\"" 36 | metric_transformation { 37 | name = each.key 38 | namespace = "${var.app_name}-error" 39 | value = 1 40 | default_value = 0 41 | } 42 | } 43 | 44 | resource "aws_cloudwatch_dashboard" "main" { 45 | dashboard_name = "${var.app_name}-dashboard" 46 | dashboard_body = templatefile("dashboard.json.template", { 47 | out_of_stock = jsonencode(local.metrics.out_of_stock) 48 | error = jsonencode(local.metrics.error) 49 | region = var.region 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Terraform for AWS Fargate 2 | 3 | Here is a configurable terraform to get you up and running with the streetmerchant docker image in AWS ECS Fargate. 4 | 5 | Running on cloud infrastructure (your mileage may vary), you'll need to integrate with one of the chat notifications rather than having your local browser navigate to a URL for you. 6 | 7 | The author's findings were that it worked ok; running the container from within EU-West-2 region was sufficient to get a timely alert for PS5 stock on Aamzon and follow the link to a successful checkout. 8 | 9 | Dependencies: 10 | 11 | - Terraform 14 12 | 13 | ## Getting started 14 | 15 | There's an example tfvars file to start you off; rename this with your own preferences. Anything you can set in the `dotenv` file you'll need to set in terraform.tfvars to get the env vars into your fargate container. 16 | 17 | Authenticate yourself with your own AWS account as with any aws commandline tool. 18 | 19 | If you wish, add a specific section to your aws credentials file and set that profile name in `terraform.tfvars`. More information on how to configure the AWS credentials file can be found in here. 20 | 21 | Then you can: 22 | 23 | ```shell 24 | cd ./terraform 25 | terraform init 26 | 27 | terraform plan 28 | terraform apply 29 | ``` 30 | 31 | ## What's included 32 | 33 | - container running streetmerchant with your chosen config 34 | - cloud metrics and a dashboard tracking 'out of stock' and 'error' responses from your configured stores 35 | -------------------------------------------------------------------------------- /.github/workflows/nightly-release.yaml: -------------------------------------------------------------------------------- 1 | name: Nightly Release 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | workflow_dispatch: {} 6 | jobs: 7 | check_code_change: 8 | name: Check code change 9 | runs-on: ubuntu-latest 10 | outputs: 11 | should_run: ${{ steps.code_change.outputs.should_run }} 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | - name: Check if code has changed with 24 hours 16 | continue-on-error: true 17 | id: code_change 18 | run: | 19 | if [[ $(git rev-list --after="24 hours" --first-parent HEAD) ]]; then 20 | echo "should_run=true" >> $GITHUB_OUTPUT 21 | fi 22 | build_tag_publish: 23 | name: Build, tag and publish Docker image 24 | runs-on: ubuntu-latest 25 | needs: check_code_change 26 | if: needs.check_code_change.outputs.should_run == 'true' 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | - name: Login to GitHub Container Registry 31 | uses: docker/login-action@v2 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Get short SHA 37 | id: short_sha 38 | run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" 39 | - name: Build and push Docker image 40 | uses: docker/build-push-action@v3 41 | with: 42 | context: . 43 | push: true 44 | tags: "ghcr.io/jef/streetmerchant:nightly,ghcr.io/jef/streetmerchant:${{ steps.short_sha.outputs.short_sha }}" 45 | -------------------------------------------------------------------------------- /src/store/model/helpers/backoff.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '..'; 2 | import {Print, logger} from '../../../logger'; 3 | import {delay, isStatusCodeInRange} from '../../../util'; 4 | import {config} from '../../../config'; 5 | 6 | type Backoff = { 7 | count: number; 8 | time: number; 9 | }; 10 | 11 | const stores: Record = {}; 12 | 13 | export async function processBackoffDelay( 14 | store: Store, 15 | link: Link, 16 | statusCode: number 17 | ): Promise { 18 | /** 19 | * We treat statusCode 0 as successful as some of the puppeteer plugins 20 | * cause side-effects resulting in an empty response object even though 21 | * the page renders fine and its content is accessible. 22 | */ 23 | 24 | let {backoffStatusCodes} = store; 25 | 26 | if (!backoffStatusCodes) { 27 | backoffStatusCodes = [403]; 28 | } 29 | 30 | const isBackoff = isStatusCodeInRange(statusCode, backoffStatusCodes); 31 | let backoff = stores[store.name]; 32 | 33 | if (!backoff) { 34 | backoff = {count: 0, time: config.browser.minBackoff}; 35 | stores[store.name] = backoff; 36 | } 37 | 38 | if (!isBackoff) { 39 | if (backoff.count > 0) { 40 | backoff.count--; 41 | backoff.time = Math.max(backoff.time / 2, config.browser.minBackoff); 42 | } 43 | 44 | return -1; 45 | } 46 | 47 | const backoffTime = backoff.time; 48 | logger.debug( 49 | Print.backoff(link, store, {delay: backoffTime, statusCode}, true) 50 | ); 51 | 52 | await delay(backoff.time); 53 | 54 | backoff.count++; 55 | backoff.time = Math.min(backoff.time * 2, config.browser.maxBackoff); 56 | 57 | return backoffTime; 58 | } 59 | -------------------------------------------------------------------------------- /src/messaging/email.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import Mail from 'nodemailer/lib/mailer'; 4 | import {config} from '../config'; 5 | import nodemailer from 'nodemailer'; 6 | 7 | const {email} = config.notifications; 8 | 9 | const transportOptions: any = {}; 10 | 11 | if (email.username && (email.password || email.smtpAddress)) { 12 | transportOptions.auth = {}; 13 | transportOptions.auth.user = email.username; 14 | transportOptions.auth.pass = email.password; 15 | } 16 | 17 | if (email.smtpAddress) { 18 | transportOptions.host = email.smtpAddress; 19 | transportOptions.port = email.smtpPort; 20 | } else { 21 | transportOptions.service = 'gmail'; 22 | } 23 | 24 | export const transporter = nodemailer.createTransport({ 25 | ...transportOptions, 26 | }); 27 | 28 | export function sendEmail(link: Link, store: Store) { 29 | if (email.username && (email.password || email.smtpAddress)) { 30 | logger.debug('↗ sending email'); 31 | 32 | const mailOptions: Mail.Options = { 33 | attachments: link.screenshot 34 | ? [ 35 | { 36 | filename: link.screenshot, 37 | path: `./${link.screenshot}`, 38 | }, 39 | ] 40 | : undefined, 41 | from: email.username, 42 | subject: Print.inStock(link, store), 43 | text: Print.productInStock(link), 44 | to: email.to, 45 | }; 46 | 47 | transporter.sendMail(mailOptions, error => { 48 | if (error) { 49 | logger.error("✖ couldn't send email", error); 50 | } else { 51 | logger.info('✔ email sent'); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/store/model/amd-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmdCa: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: '.btn-shopping-cart', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '.product-page-description h4', 12 | euroFormat: false, 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.amd.com/en/direct-buy/5458373400/ca', 21 | }, 22 | { 23 | brand: 'amd', 24 | cartUrl: 25 | 'https://www.amd.com/en/direct-buy/5458373400/ca?add-to-cart=true', 26 | model: 'amd reference', 27 | series: 'rx6800', 28 | url: 'https://www.amd.com/en/direct-buy/5458373400/ca', 29 | }, 30 | { 31 | brand: 'amd', 32 | cartUrl: 33 | 'https://www.amd.com/en/direct-buy/5458372800/ca?add-to-cart=true', 34 | model: 'amd reference', 35 | series: 'rx6800xt', 36 | url: 'https://www.amd.com/en/direct-buy/5458372800/ca', 37 | }, 38 | { 39 | brand: 'amd', 40 | cartUrl: 41 | 'https://www.amd.com/en/direct-buy/5458372200/ca?add-to-cart=true', 42 | model: 'amd reference', 43 | series: 'rx6900xt', 44 | url: 'https://www.amd.com/en/direct-buy/5458372200/ca', 45 | }, 46 | { 47 | brand: 'amd', 48 | cartUrl: 49 | 'https://www.amd.com/en/direct-buy/5496921500/ca?add-to-cart=true', 50 | model: 'amd reference', 51 | series: 'rx6800xt', 52 | url: 'https://www.amd.com/en/direct-buy/5496921500/ca', 53 | }, 54 | ], 55 | name: 'amd-ca', 56 | country: 'CA', 57 | }; 58 | -------------------------------------------------------------------------------- /src/store/model/walmart-ca.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const WalmartCa: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: '[data-automation="cta-button"]', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '[data-automation="buybox-price"]', 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.walmart.ca/en/ip/sandisk-64g-ultra-sdxc-uhs-1-memory-card-80mbs-c10-u1-full-hd-sd-card-sdsdunc-064g-cw6in/6000200452075', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.walmart.ca/en/ip/playstation5-console/6000202198562', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.walmart.ca/en/ip/playstation5-digital-edition/6000202198823', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.walmart.ca/en/ip/xbox-series-x/6000201786332', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.walmart.ca/en/ip/xbox-series-s/6000201790919', 44 | }, 45 | { 46 | brand: 'corsair', 47 | model: '600 platinum', 48 | series: 'sf', 49 | url: 'https://www.walmart.ca/en/ip/Corsair-SF-Series-SF600-600-Watt-80-PLUS-Gold-Certified-High-Performance-SFX-PSU/PRD6VH8WNKHD36Q', 50 | }, 51 | ], 52 | name: 'walmart-ca', 53 | country: 'CA', 54 | }; 55 | -------------------------------------------------------------------------------- /src/store/model/expert.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Expert: Store = { 4 | backoffStatusCodes: [403, 429, 503], 5 | currency: '€', 6 | labels: { 7 | inStock: [ 8 | { 9 | container: 'span.widget-ArticleStatus-buttonText', 10 | text: ['In den Warenkorb'], 11 | }, 12 | ], 13 | maxPrice: { 14 | container: '.widget-Container-subContent .widget-ArticlePrice-price', 15 | euroFormat: false, 16 | }, 17 | outOfStock: [ 18 | { 19 | container: 20 | 'span[style="font-size: 14pt;"] > span[style="color: #ff5e19;"]', 21 | text: ['Das von Ihnen ausgewählte Produkt ist ausverkauft'], 22 | }, 23 | { 24 | container: 'span.widget-ArticleStatus-statusPointText', 25 | text: ['Artikel ist derzeit nicht verfügbar'], 26 | }, 27 | ], 28 | }, 29 | links: [ 30 | { 31 | brand: 'test:brand', 32 | model: 'test:model', 33 | series: 'test:series', 34 | url: 'https://www.expert.de/shop/11364114744-ps4-pro-1tb-jet-black.html', 35 | }, 36 | { 37 | brand: 'sony', 38 | model: 'ps5 console', 39 | series: 'sonyps5c', 40 | url: 'https://www.expert.de/shop/11364129744-playstation-r-5.html', 41 | }, 42 | { 43 | brand: 'sony', 44 | model: 'ps5 digital', 45 | series: 'sonyps5de', 46 | url: 'https://www.expert.de/shop/11364133744-playstation-r-5-digital-edition.html', 47 | }, 48 | { 49 | brand: 'microsoft', 50 | model: 'xbox series s', 51 | series: 'xboxss', 52 | url: 'https://www.expert.de/shop/11350018530-xbox-series-s.html', 53 | }, 54 | ], 55 | name: 'expert', 56 | country: 'DE', 57 | }; 58 | -------------------------------------------------------------------------------- /src/messaging/ntfy.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import {config} from '../config'; 4 | import fetch from 'node-fetch'; 5 | 6 | const {ntfy} = config.notifications; 7 | 8 | export function sendNtfyAlert(link: Link, store: Store) { 9 | if (ntfy.topic) { 10 | logger.debug('↗ sending ntfy alert'); 11 | 12 | (async () => { 13 | const message = `${Print.inStock(link, store)}`; 14 | const headers: Record = {}; 15 | 16 | if (ntfy.priority) headers['Priority'] = ntfy.priority; 17 | headers[ 18 | 'Tags' 19 | ] = `${store.name},${link.model},${link.series},${link.brand}`; 20 | if (ntfy.title) headers['Title'] = ntfy.title; 21 | if (ntfy.accessToken) 22 | headers['Authorization'] = `Bearer ${ntfy.accessToken}`; 23 | 24 | const body = { 25 | topic: ntfy.topic, 26 | message, 27 | actions: [ 28 | { 29 | action: 'view', 30 | label: 'Add to cart', 31 | url: link.cartUrl ?? link.url, 32 | }, 33 | ], 34 | }; 35 | 36 | try { 37 | const response = await fetch(ntfy.url, { 38 | method: 'POST', 39 | body: JSON.stringify(body), 40 | headers: { 41 | ...headers, 42 | 'Content-Type': 'application/json', 43 | }, 44 | }); 45 | 46 | if (!response.ok) 47 | throw new Error(`Failed to send ntfy alert: ${response.statusText}`); 48 | 49 | logger.info('✔ ntfy alert sent'); 50 | } catch (error: unknown) { 51 | logger.error("✖ couldn't send ntfy alert", error); 52 | } 53 | })(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/store/model/target.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Target: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: '[data-test="preorderButton"]', 9 | text: ['Preorder now'], 10 | }, 11 | { 12 | container: '[data-test="shipItButton"]', 13 | text: ['Ship it'], 14 | }, 15 | ], 16 | maxPrice: { 17 | container: '[data-test="product-price"]', 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.target.com/p/playstation-5-console/-/A-87716467', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.target.com/p/playstation-5-digital-edition-console/-/A-81114596', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.target.com/p/xbox-series-x-console/-/A-80790841', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.target.com/p/xbox-series-s-console/-/A-80790842', 44 | }, 45 | { 46 | brand: 'nintendo', 47 | model: 'switch 2', 48 | series: 'switch2', 49 | url: 'https://www.target.com/p/nintendo-switch-2-console/-/A-94693225', 50 | }, 51 | { 52 | brand: 'nintendo', 53 | model: 'switch 2 bundle', 54 | series: 'switch2', 55 | url: 'https://www.target.com/p/nintendo-switch-2-console-mario-kart-world-bundle/-/A-94693226', 56 | }, 57 | ], 58 | name: 'target', 59 | country: 'US', 60 | }; 61 | -------------------------------------------------------------------------------- /src/store/model/allneeds.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Allneeds: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: '.amstockstatus', 9 | text: ['In Stock'], 10 | }, 11 | maxPrice: { 12 | container: 'span.price', 13 | euroFormat: false, 14 | }, 15 | outOfStock: { 16 | container: '.amstockstatus', 17 | text: ['sold out'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'asus', 23 | model: 'strix oc white', 24 | series: '3080', 25 | url: 'https://allneeds.com.au/asus-geforce-rtx-3080-rog-strix-gaming-oc-10gb-video-card-white-edition-limited-edition', 26 | }, 27 | { 28 | brand: 'sapphire', 29 | model: 'nitro+', 30 | series: 'rx6900xt', 31 | url: 'https://allneeds.com.au/sapphire-nitro-radeon-rx-6900-xt-16gb-video-card-11308-01-20g', 32 | }, 33 | { 34 | brand: 'amd', 35 | model: '5950x', 36 | series: 'ryzen5950', 37 | url: 'https://allneeds.com.au/amd-ryzen-9-5950x-processor-100-100000059wof', 38 | }, 39 | { 40 | brand: 'amd', 41 | model: '5900x', 42 | series: 'ryzen5900', 43 | url: 'https://allneeds.com.au/amd-ryzen-9-5900x-processor-100-100000061wof', 44 | }, 45 | { 46 | brand: 'amd', 47 | model: '5800x', 48 | series: 'ryzen5800', 49 | url: 'https://allneeds.com.au/amd-ryzen-7-5800x-processor', 50 | }, 51 | { 52 | brand: 'amd', 53 | model: '5600x', 54 | series: 'ryzen5600', 55 | url: 'https://allneeds.com.au/amd-ryzen-5-5600x-with-wraith-stealth-100-100000065box', 56 | }, 57 | ], 58 | name: 'allneeds', 59 | country: 'AU', 60 | }; 61 | -------------------------------------------------------------------------------- /src/messaging/pushover.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import Push, {PushoverMessage} from 'pushover-notifications'; 4 | import {config} from '../config'; 5 | 6 | const {pushover} = config.notifications; 7 | 8 | export function sendPushoverNotification(link: Link, store: Store) { 9 | if (pushover.token && pushover.username) { 10 | logger.debug('↗ sending pushover message'); 11 | 12 | const push = new Push({ 13 | token: pushover.token, 14 | user: pushover.username, 15 | }); 16 | 17 | const message: PushoverMessage = 18 | pushover.priority < 2 19 | ? { 20 | message: 21 | link.cartUrl && config.store.autoAddToCart 22 | ? link.cartUrl 23 | : link.url, 24 | priority: pushover.priority, 25 | sound: pushover.sound, 26 | title: Print.inStock(link, store), 27 | device: pushover.device, 28 | ...(link.screenshot && {file: `./${link.screenshot}`}), 29 | } 30 | : { 31 | expire: pushover.expire, 32 | message: 33 | link.cartUrl && config.store.autoAddToCart 34 | ? link.cartUrl 35 | : link.url, 36 | priority: pushover.priority, 37 | sound: pushover.sound, 38 | retry: pushover.retry, 39 | title: Print.inStock(link, store), 40 | device: pushover.device, 41 | ...(link.screenshot && {file: `./${link.screenshot}`}), 42 | }; 43 | 44 | push.send(message, (error: Error) => { 45 | if (error) { 46 | logger.error("✖ couldn't send pushover message", error); 47 | } else { 48 | logger.info('✔ pushover message sent'); 49 | } 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/store/model/harristechnology.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const HarrisTechnology: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: 9 | '#content_tab-description > div.desc2 > p.product-availability', 10 | text: ['in stock'], 11 | }, 12 | outOfStock: { 13 | container: 14 | '#content_tab-description > div.desc2 > p.product-availability', 15 | text: ['Please call or e-mail us for availability'], 16 | }, 17 | }, 18 | links: [ 19 | { 20 | brand: 'gigabyte', 21 | model: 'gaming oc', 22 | series: '3080', 23 | url: 'https://www.ht.com.au/part/BY216-Gigabyte-GeForce-RTX-3080-GAMING-OC-10GB-Video-Card/detail.hts', 24 | }, 25 | { 26 | brand: 'gigabyte', 27 | model: 'vision oc', 28 | series: '3080', 29 | url: 'https://www.ht.com.au/part/BZ284-Gigabyte-nVidia-GeForce-RTX-3080-VISION-OC-10G-GDDR6X-1800-MHz-PCIE4.0x16-7680x432060Hz-3xDP-2xHDMI/detail.hts', 30 | }, 31 | { 32 | brand: 'gigabyte', 33 | model: 'aorus master', 34 | series: '3080', 35 | url: 'https://www.ht.com.au/part/BZ346-Gigabyte-nVidia-GeForce-RTX-3080-MASTER-AORUS-10G-GDDR6X-1845-MHz-3xDP-3xHDMI/detail.hts', 36 | }, 37 | { 38 | brand: 'msi', 39 | model: 'gaming x trio', 40 | series: '3080', 41 | url: 'https://www.ht.com.au/part/BY193-MSI-GeForce-RTX-3080-GAMING-X-TRIO-10GB-Video-Card/detail.hts', 42 | }, 43 | { 44 | brand: 'amd', 45 | model: '5600x', 46 | series: 'ryzen5600', 47 | url: 'https://www.ht.com.au/part/CA093-AMD-Ryzen-5-5600X-6-Core-3.7-GHz-Desktop-Processor-with-AM4-Socket-65W-Thermal-Design-Power/detail.hts', 48 | }, 49 | ], 50 | name: 'harristechnology', 51 | country: 'AU', 52 | }; 53 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | [![streetmerchant](assets/images/streetmerchant-banner.png)](https://jef.buzz/streetmerchant) 2 | 3 | ## Features 4 | 5 | First and foremost, this service _will not_ automatically buy for you. 6 | 7 | - **Checks stock continuously** -- runs 24/7, 365, looking for the items you want. 8 | - **Ready for checkout** -- ability to add to cart when available and even opens the browser for you. 9 | - **Notifications galore** -- when you're not by your computer, worry free with notifications to most platforms and devices when an item comes in stock. 10 | 11 | ## Getting started 12 | 13 | You'll find most of the content on the left sidebar. The right sidebar will help you navigate a page. 14 | 15 | ### Contributing 16 | 17 | - Give helpful tips and tricks to the [community based wiki](https://github.com/jef/streetmerchant/wiki). 18 | - Add to the documentation through [pull requests](https://github.com/jef/streetmerchant/pulls). 19 | - Fork and make a pull request to the repository. 20 | 21 | ### Looking for help 22 | 23 | - Have an idea, question, or need help? Visit our [GitHub discussion board](https://github.com/jef/streetmerchant/discussions). 24 | - Ran into a bug? File a [GitHub issue](https://github.com/jef/streetmerchant/issues/new/choose). 25 | - Looking to hang out and talk shop? Join us on [Discord](https://discord.gg/gbVY4vB9JF). 26 | 27 | ### Supporting the project 28 | 29 | The best way to support me is to donate to [Diabetes Research Institute](https://www.diabetesresearch.org/Give). 30 | 31 | > The Diabetes Research Institute leads the world in cure-focused diabetes research. 32 | > 33 | > [diabetesresearch.org](https://www.diabetesresearch.org/about-DRI) 34 | 35 | If you feel inclined to support me directly, here are those options: 36 | 37 | - [GitHub Sponsors](https://github.com/sponsors/jef) 38 | - [Paypal](https://www.paypal.me/jxf) 39 | -------------------------------------------------------------------------------- /src/store/model/ubiquiti.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Ubiquiti: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: '#titleInStockBadge', 9 | text: ['In Stock'], 10 | }, 11 | ], 12 | outOfStock: [ 13 | { 14 | container: '.titleSoldOutBadge', 15 | text: ['Sold Out'], 16 | }, 17 | { 18 | container: '#titleSoldOutBadge', 19 | text: ['Sold Out'], 20 | }, 21 | ], 22 | }, 23 | links: [ 24 | { 25 | brand: 'ubiquiti', 26 | model: 'dream machine', 27 | series: 'udm-us', 28 | url: 'https://store.ui.com/collections/unifi-network-unifi-os-consoles/products/udm-us', 29 | }, 30 | { 31 | brand: 'ubiquiti', 32 | model: 'dream machine pro', 33 | series: 'udm-pro', 34 | url: 'https://store.ui.com/collections/unifi-network-unifi-os-consoles/products/udm-pro', 35 | }, 36 | { 37 | brand: 'ubiquiti', 38 | model: 'dream router', 39 | series: 'udr-us', 40 | url: 'https://store.ui.com/collections/unifi-network-unifi-os-consoles/products/dream-router', 41 | }, 42 | { 43 | brand: 'ubiquiti', 44 | model: 'g4 doorbell pro', 45 | series: 'g4-doorbell-pro', 46 | url: 'https://store.ui.com/collections/unifi-protect/products/g4-doorbell-pro', 47 | }, 48 | { 49 | brand: 'ubiquiti', 50 | model: 'network video recorder', 51 | series: 'unvr', 52 | url: 'https://store.ui.com/collections/unifi-protect/products/unvr', 53 | }, 54 | { 55 | brand: 'ubiquiti', 56 | model: 'network video recorder pro', 57 | series: 'unvr-pro', 58 | url: 'https://store.ui.com/collections/unifi-protect/products/unvr-pro', 59 | }, 60 | ], 61 | name: 'ubiquiti', 62 | country: 'US', 63 | }; 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: 5 | - 'type: bug' 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | attributes: 12 | label: Expected Behavior 13 | description: What did you expect to happen? 14 | placeholder: Tell us what you see! 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Current Behavior 20 | description: What happened? 21 | placeholder: Tell us what you see! 22 | validations: 23 | required: true 24 | - type: dropdown 25 | attributes: 26 | label: What operating system are you seeing the problem on? 27 | multiple: true 28 | options: 29 | - Linux 30 | - Windows 31 | - macOS 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Relevant log output 37 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 38 | render: shell 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Configuration 44 | description: Please copy and paste your `dotenv`. This will be automatically formatted into code, so no need for backticks. 45 | render: shell 46 | validations: 47 | required: true 48 | - type: checkboxes 49 | attributes: 50 | label: Code of Conduct 51 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/jef/streetmerchant/blob/main/CODE_OF_CONDUCT.md) 52 | options: 53 | - label: I agree to follow this project's Code of Conduct 54 | required: true 55 | -------------------------------------------------------------------------------- /terraform/resource-ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "main" { 2 | name = "${var.app_name}-ecs-cluster" 3 | } 4 | 5 | resource "aws_ecs_service" "main" { 6 | name = "${var.app_name}-ecs-service" 7 | cluster = aws_ecs_cluster.main.id 8 | task_definition = aws_ecs_task_definition.main.id 9 | desired_count = 1 10 | network_configuration { 11 | subnets = [ 12 | aws_subnet.aws-subnet.id 13 | ] 14 | assign_public_ip = true 15 | } 16 | launch_type = "FARGATE" 17 | } 18 | 19 | data "aws_iam_policy_document" "ecs_task_execution_role" { 20 | version = "2012-10-17" 21 | statement { 22 | sid = "" 23 | effect = "Allow" 24 | actions = ["sts:AssumeRole"] 25 | 26 | principals { 27 | type = "Service" 28 | identifiers = ["ecs-tasks.amazonaws.com"] 29 | } 30 | } 31 | } 32 | 33 | resource "aws_iam_role" "ecs_task_execution_role" { 34 | name = var.ecs_task_execution_role_name 35 | assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json 36 | } 37 | 38 | resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" { 39 | role = aws_iam_role.ecs_task_execution_role.name 40 | policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 41 | } 42 | 43 | locals { 44 | container_env = [for k, v in var.streetmerchant_env : { name: k, value: v}] 45 | } 46 | 47 | resource "aws_ecs_task_definition" "main" { 48 | container_definitions = templatefile("taskdef.json.template", { 49 | "name": var.app_name 50 | "awslogs-group": aws_cloudwatch_log_group.main.name 51 | "region": var.region 52 | "cpu": var.cpu 53 | "memory": parseint(var.memory,10) 54 | "environment": jsonencode(local.container_env) 55 | }) 56 | family = var.app_name 57 | requires_compatibilities = ["FARGATE"] 58 | network_mode = "awsvpc" 59 | cpu = var.cpu 60 | memory = var.memory 61 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 62 | } 63 | -------------------------------------------------------------------------------- /src/store/model/playstation.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import fetch from 'node-fetch'; 3 | 4 | export const PlayStation: Store = { 5 | currency: '$', 6 | labels: { 7 | inStock: [ 8 | { 9 | container: '.productHero-desc .add-to-cart:not(.hide)', 10 | text: ['Add'], 11 | }, 12 | { 13 | container: '.bulleted-info.queue', 14 | text: ['queue'], 15 | }, 16 | ], 17 | outOfStock: { 18 | container: '.productHero-info .out-stock-wrpr:not(.hide)', 19 | text: ['Out of Stock'], 20 | }, 21 | }, 22 | links: [ 23 | { 24 | brand: 'test:brand', 25 | itemNumber: '3005715', 26 | model: 'test:model', 27 | series: 'test:series', 28 | url: 'https://direct.playstation.com/en-us/accessories/accessory/dualsense-wireless-controller.3005715', 29 | }, 30 | { 31 | brand: 'sony', 32 | itemNumber: '3005816', 33 | model: 'ps5 console', 34 | series: 'sonyps5c', 35 | url: 'https://direct.playstation.com/en-us/consoles/console/playstation5-console.3005816', 36 | }, 37 | { 38 | brand: 'sony', 39 | itemNumber: '3005817', 40 | model: 'ps5 digital', 41 | series: 'sonyps5de', 42 | url: 'https://direct.playstation.com/en-us/consoles/console/playstation5-digital-edition-console.3005817', 43 | }, 44 | ], 45 | name: 'playstation', 46 | country: 'US', 47 | realTimeInventoryLookup: async (itemNumber: string) => { 48 | const request_url = 49 | 'https://api.direct.playstation.com/commercewebservices/ps-direct-us/products/productList?fields=BASIC&productCodes=' + 50 | itemNumber; 51 | const response = await fetch(request_url); 52 | const response_json = await response.json(); 53 | if ( 54 | response_json.products[0].stock.stockLevelStatus !== 'outOfStock' && 55 | response_json.products[0].maxOrderQuantity >= 0 56 | ) { 57 | return true; 58 | } 59 | 60 | return false; 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/store/model/amazon-de-warehouse.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmazonDeWarehouse: Store = { 4 | backoffStatusCodes: [403, 429, 503], 5 | currency: '€', 6 | labels: { 7 | captcha: { 8 | container: 'body', 9 | text: [ 10 | 'geben sie die unten angezeigten zeichen ein', 11 | 'geben sie die zeichen unten ein', 12 | ], 13 | }, 14 | captchaHandler: { 15 | challenge: '.a-row > img', 16 | input: '#captchacharacters', 17 | submit: 'button[type="submit"]', 18 | }, 19 | inStock: { 20 | container: '#a-autoid-0-announce', 21 | text: ['In den Einkaufswagen'], 22 | }, 23 | maxPrice: { 24 | container: '.olpOfferPrice', 25 | euroFormat: true, 26 | }, 27 | outOfStock: [ 28 | { 29 | container: '.a-size-medium', 30 | text: [ 31 | 'Derzeit gibt es keine Verkäufer, die diesen Artikel an Ihren Standort liefern können', 32 | ], 33 | }, 34 | ], 35 | }, 36 | links: [ 37 | { 38 | brand: 'test:brand', 39 | model: 'test:model', 40 | series: 'test:series', 41 | url: 'https://www.amazon.de/gp/offer-listing/B07PW9VBK5', 42 | }, 43 | { 44 | brand: 'sony', 45 | model: 'ps5 console', 46 | series: 'sonyps5c', 47 | url: 'https://www.amazon.de/gp/offer-listing/B08H93ZRK9', 48 | }, 49 | { 50 | brand: 'sony', 51 | model: 'ps5 digital', 52 | series: 'sonyps5de', 53 | url: 'https://www.amazon.de/gp/offer-listing/B08H98GVK8', 54 | }, 55 | { 56 | brand: 'microsoft', 57 | model: 'xbox series s', 58 | series: 'xboxss', 59 | url: 'https://www.amazon.de/gp/offer-listing/B087VM5XC6', 60 | }, 61 | { 62 | brand: 'microsoft', 63 | model: 'xbox series x', 64 | series: 'xboxsx', 65 | url: 'https://www.amazon.de/gp/offer-listing/B08H93ZRLL', 66 | }, 67 | ], 68 | name: 'amazon-de-warehouse', 69 | country: 'DE', 70 | }; 71 | -------------------------------------------------------------------------------- /docs/help/troubleshoot.md: -------------------------------------------------------------------------------- 1 | # Troubleshoot 2 | 3 | ## Captcha issues 4 | 5 | ???+ info 6 | A new interactive captcha handler has been implemented. You can learn more about how to use it [here](../reference/captcha.md)! Otherwise, feel free to still try the below options. 7 | 8 | ### Option 1 9 | 10 | If you're running into problems, try running in headful mode: `HEADLESS="false"`. 11 | 12 | This will open a browser and run streetmerchant. Note that this isn't a great solution for those running in a headless environment, i.e.: VPS, cloud, docker. Instead, it would be a good solution for those running on separate computer that won't be blocked by running in the background. 13 | 14 | ### Option 2 15 | 16 | As a last case scenario, use `PUPPETEER_EXECUTABLE_PATH`. This will use your computer's Chrome browser. You can run this is headless or headful mode. 17 | 18 | > From the puppeteer doc: 19 | > 20 | > `PUPPETEER_EXECUTABLE_PATH` - specify an executable path to be used in `puppeteer.launch`. See [puppeteer.launch([options])](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#puppeteerlaunchoptions) on how the executable path is inferred. **BEWARE**: Puppeteer is only [guaranteed to work](https://github.com/puppeteer/puppeteer/#q-why-doesnt-puppeteer-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. 21 | 22 | For example: 23 | 24 | `dotenv`: 25 | 26 | ``` 27 | PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable 28 | ``` 29 | 30 | This will vary depending on your operating system and install path. Please use full paths. 31 | 32 | ## macOS code signing 33 | 34 | If you're getting a popup like this: 35 | 36 | ![image](https://user-images.githubusercontent.com/12074633/93616357-a36bf180-f9a2-11ea-82fa-da2a44807802.png) 37 | 38 | Then run this command: 39 | 40 | ```sh 41 | sudo codesign --force --deep --sign - ./node_modules/puppeteer/.local-chromium/mac-800071/chrome-mac/Chromium.app 42 | ``` 43 | 44 | ???+ tip 45 | The `mac-800071` may be different on your machine, so I would start from `./node_modules/puppeteer/.local-chromium` and auto complete from there. 46 | -------------------------------------------------------------------------------- /src/store/model/euronics-de.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const EuronicsDE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.buy-btn--cart-text', 8 | text: ['Warenkorb'], 9 | }, 10 | maxPrice: { 11 | container: '.price--content', 12 | euroFormat: true, 13 | }, 14 | outOfStock: { 15 | container: 16 | '.product--buybox .alert.is--error.is--rounded .alert--content', 17 | text: [ 18 | 'Artikel steht derzeit nicht zur Verfügung', 19 | 'Morgen im Laufe des Morgens und nur online unter', 20 | ], 21 | }, 22 | }, 23 | links: [ 24 | { 25 | brand: 'test:brand', 26 | model: 'test:model', 27 | series: 'test:series', 28 | url: 'https://www.euronics.de/telefon-und-navigation/festnetz/schnurlose-telefone/kx-tg6721gb-schnurlostelefon-mit-anrufbeantworter-schwarz-4051168442801', 29 | }, 30 | { 31 | brand: 'microsoft', 32 | model: 'xbox series s', 33 | series: 'xboxss', 34 | url: 'https://www.euronics.de/spiele-und-konsolen-film-und-musik/spiele-und-konsolen/xbox-series-x/spielekonsole/xbox-series-s-512gb-konsole-4061856838076', 35 | }, 36 | { 37 | brand: 'microsoft', 38 | model: 'xbox series x', 39 | series: 'xboxsx', 40 | url: 'https://www.euronics.de/spiele-und-konsolen-film-und-musik/spiele-und-konsolen/xbox-series-x/spielekonsole/xbox-series-x-1tb-konsole-4061856838045', 41 | }, 42 | { 43 | brand: 'sony', 44 | model: 'ps5 digital', 45 | series: 'sonyps5de', 46 | url: 'https://www.euronics.de/spiele-und-konsolen-film-und-musik/spiele-und-konsolen/playstation-5/spielekonsole/playstation-5-digital-edition-konsole-4061856837833', 47 | }, 48 | { 49 | brand: 'sony', 50 | model: 'ps5 console', 51 | series: 'sonyps5c', 52 | url: 'https://www.euronics.de/spiele-und-konsolen-film-und-musik/spiele-und-konsolen/playstation-5/spielekonsole/playstation-5-konsole-4061856837826', 53 | }, 54 | ], 55 | name: 'euronics-de', 56 | country: 'DE', 57 | }; 58 | -------------------------------------------------------------------------------- /src/store/model/evga-eu.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const EvgaEu: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.product-buy-specs', 8 | text: ['add to cart'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'evga', 14 | model: 'ftw3', 15 | series: '3080', 16 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3895-KR', 17 | }, 18 | { 19 | brand: 'evga', 20 | model: 'ftw3 ultra', 21 | series: '3080', 22 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3897-KR', 23 | }, 24 | { 25 | brand: 'evga', 26 | model: 'xc3', 27 | series: '3080', 28 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3883-KR', 29 | }, 30 | { 31 | brand: 'evga', 32 | model: 'xc3 black', 33 | series: '3080', 34 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3881-KR', 35 | }, 36 | { 37 | brand: 'evga', 38 | model: 'xc3 ultra', 39 | series: '3080', 40 | url: 'https://eu.evga.com/products/product.aspx?pn=10G-P5-3885-KR', 41 | }, 42 | { 43 | brand: 'evga', 44 | model: 'ftw3', 45 | series: '3090', 46 | url: 'https://eu.evga.com/products/product.aspx?pn=24G-P5-3985-KR', 47 | }, 48 | { 49 | brand: 'evga', 50 | model: 'ftw3 ultra', 51 | series: '3090', 52 | url: 'https://eu.evga.com/products/product.aspx?pn=24G-P5-3987-KR', 53 | }, 54 | { 55 | brand: 'evga', 56 | model: 'xc3', 57 | series: '3090', 58 | url: 'https://eu.evga.com/products/product.aspx?pn=24G-P5-3973-KR', 59 | }, 60 | { 61 | brand: 'evga', 62 | model: 'xc3 black', 63 | series: '3090', 64 | url: 'https://eu.evga.com/products/product.aspx?pn=24G-P5-3971-KR', 65 | }, 66 | { 67 | brand: 'evga', 68 | model: 'xc3 ultra', 69 | series: '3090', 70 | url: 'https://eu.evga.com/products/product.aspx?pn=24G-P5-3975-KR', 71 | }, 72 | ], 73 | name: 'evga-eu', 74 | country: 'EU', 75 | }; 76 | -------------------------------------------------------------------------------- /src/store/model/elcorteingles.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Elcorteingles: Store = { 4 | backoffStatusCodes: [403, 429, 503], 5 | currency: '€', 6 | labels: { 7 | // Captcha: { 8 | // container: 'body', 9 | // text: [ 10 | // 'geben sie die unten angezeigten zeichen ein', 11 | // 'geben sie die zeichen unten ein' 12 | // ] 13 | // }, 14 | inStock: [ 15 | { 16 | container: '.product_detail-purchase.mb-2.c12 .js-add-cart-text', 17 | text: ['a la cesta'], 18 | }, 19 | ], 20 | maxPrice: { 21 | container: '.product_detail-buy-price-container .price._big', 22 | euroFormat: true, 23 | }, 24 | outOfStock: [ 25 | { 26 | container: 27 | '.c12.mt-2.product_detail-add_to_cart.one_click_enabled .c12.button._normal.js-buy-button._sold_out.view-page._disabled', 28 | text: ['Agotado'], 29 | }, 30 | { 31 | container: 32 | '.product_detail-purchase.mb-2.c12 .c12.button._normal.js-buy-button._sold_out.view-page._disabled', 33 | text: ['No disponible'], 34 | }, 35 | ], 36 | }, 37 | links: [ 38 | { 39 | brand: 'test:brand', 40 | model: 'test:model', 41 | series: 'test:series', 42 | url: 'https://www.elcorteingles.es/moda/A26324406/', 43 | }, 44 | { 45 | brand: 'sony', 46 | model: 'ps5 console', 47 | series: 'sonyps5c', 48 | url: 'https://www.elcorteingles.es/videojuegos/A37046604', 49 | }, 50 | { 51 | brand: 'sony', 52 | model: 'ps5 digital', 53 | series: 'sonyps5de', 54 | url: 'https://www.elcorteingles.es/videojuegos/A37046605', 55 | }, 56 | { 57 | brand: 'microsoft', 58 | model: 'xbox series x', 59 | series: 'xboxsx', 60 | url: 'https://www.elcorteingles.es/videojuegos/A37047078', 61 | }, 62 | { 63 | brand: 'microsoft', 64 | model: 'xbox series s', 65 | series: 'xboxss', 66 | url: 'https://www.elcorteingles.es/videojuegos/A37047080', 67 | }, 68 | ], 69 | name: 'elcorteingles', 70 | country: 'ES', 71 | }; 72 | -------------------------------------------------------------------------------- /src/store/filter.ts: -------------------------------------------------------------------------------- 1 | import {Link} from './model'; 2 | import {config} from '../config'; 3 | 4 | /** 5 | * Returns true if the brand should be checked for stock 6 | * 7 | * @param brand The brand of the GPU 8 | */ 9 | function filterBrand(brand: Link['brand']): boolean { 10 | if (config.store.showOnlyBrands.length === 0) { 11 | return true; 12 | } 13 | 14 | return config.store.showOnlyBrands.includes(brand); 15 | } 16 | 17 | /** 18 | * Returns true if the model should be checked for stock 19 | * 20 | * @param model The model of the GPU 21 | * @param series The series of the GPU 22 | */ 23 | function filterModel(model: Link['model'], series: Link['series']): boolean { 24 | if (config.store.showOnlyModels.length === 0) { 25 | return true; 26 | } 27 | 28 | const sanitizedModel = model.replace(/\s/g, ''); 29 | const sanitizedSeries = series.replace(/\s/g, ''); 30 | for (const configModelEntry of config.store.showOnlyModels) { 31 | const sanitizedConfigModel = configModelEntry.name.replace(/\s/g, ''); 32 | const sanitizedConfigSeries = configModelEntry.series.replace(/\s/g, ''); 33 | if (sanitizedConfigSeries) { 34 | if ( 35 | sanitizedSeries === sanitizedConfigSeries && 36 | sanitizedModel === sanitizedConfigModel 37 | ) { 38 | return true; 39 | } 40 | } else if (sanitizedModel === sanitizedConfigModel) { 41 | return true; 42 | } 43 | } 44 | 45 | return false; 46 | } 47 | 48 | /** 49 | * Returns true if the series should be checked for stock 50 | * 51 | * @param series The series of the GPU 52 | */ 53 | export function filterSeries(series: Link['series']): boolean { 54 | if (config.store.showOnlySeries.length === 0) { 55 | return true; 56 | } 57 | 58 | return config.store.showOnlySeries.includes(series); 59 | } 60 | 61 | /** 62 | * Returns true if the link should be checked for stock 63 | * 64 | * @param link The store link of the GPU 65 | */ 66 | export function filterStoreLink(link: Link): boolean { 67 | return ( 68 | filterBrand(link.brand) && 69 | filterModel(link.model, link.series) && 70 | filterSeries(link.series) 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/store/model/nvidia-gb.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NvidiaGB: Store = { 4 | currency: '£', 5 | labels: { 6 | inStock: { 7 | container: '.buy', 8 | text: ['add to cart', 'buy now'], 9 | }, 10 | outOfStock: { 11 | container: '.buy', 12 | text: ['out of stock'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.nvidia.com/en-gb/geforce/graphics-cards/rtx-2060-super/', 21 | }, 22 | { 23 | brand: 'nvidia', 24 | model: 'founders edition', 25 | series: '3060ti', 26 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203060%20Ti&manufacturer=NVIDIA', 27 | }, 28 | { 29 | brand: 'nvidia', 30 | model: 'founders edition', 31 | series: '3070', 32 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203070&manufacturer=NVIDIA', 33 | }, 34 | { 35 | brand: 'nvidia', 36 | model: 'founders edition', 37 | series: '3070ti', 38 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203070%20Ti&manufacturer=NVIDIA', 39 | }, 40 | { 41 | brand: 'nvidia', 42 | model: 'founders edition', 43 | series: '3080', 44 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203080&manufacturer=NVIDIA', 45 | }, 46 | { 47 | brand: 'nvidia', 48 | model: 'founders edition', 49 | series: '3080ti', 50 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203080%20Ti&manufacturer=NVIDIA', 51 | }, 52 | { 53 | brand: 'nvidia', 54 | model: 'founders edition', 55 | series: '3090', 56 | url: 'https://shop.nvidia.com/en-gb/geforce/store/gpu/?page=1&limit=9&locale=en-gb&category=GPU&gpu=RTX%203090&manufacturer=NVIDIA', 57 | }, 58 | ], 59 | name: 'nvidia-gb', 60 | country: 'UK', 61 | }; 62 | -------------------------------------------------------------------------------- /src/store/model/nvidia-es.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NvidiaES: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.buy', 8 | text: ['Añadir al carrito', 'COMPRAR AHORA'], 9 | }, 10 | outOfStock: { 11 | container: '.buy', 12 | text: ['AGOTADO'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.nvidia.com/es-es/geforce/graphics-cards/rtx-2060-super/', 21 | }, 22 | { 23 | brand: 'nvidia', 24 | model: 'founders edition', 25 | series: '3060ti', 26 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203060%20Ti&manufacturer=NVIDIA', 27 | }, 28 | { 29 | brand: 'nvidia', 30 | model: 'founders edition', 31 | series: '3070', 32 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203070&manufacturer=NVIDIA', 33 | }, 34 | { 35 | brand: 'nvidia', 36 | model: 'founders edition', 37 | series: '3070ti', 38 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203070%20Ti&manufacturer=NVIDIA', 39 | }, 40 | { 41 | brand: 'nvidia', 42 | model: 'founders edition', 43 | series: '3080', 44 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203080&manufacturer=NVIDIA', 45 | }, 46 | { 47 | brand: 'nvidia', 48 | model: 'founders edition', 49 | series: '3080ti', 50 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203080%20Ti&manufacturer=NVIDIA', 51 | }, 52 | { 53 | brand: 'nvidia', 54 | model: 'founders edition', 55 | series: '3090', 56 | url: 'https://shop.nvidia.com/es-es/geforce/store/gpu/?page=1&limit=9&locale=es-es&category=GPU&gpu=RTX%203090&manufacturer=NVIDIA', 57 | }, 58 | ], 59 | name: 'nvidia-es', 60 | country: 'ES', 61 | }; 62 | -------------------------------------------------------------------------------- /src/store/model/nvidia-fr.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NvidiaFR: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.buy', 8 | text: ['ajouter au panier', 'acheter maintenant'], 9 | }, 10 | outOfStock: { 11 | container: '.buy', 12 | text: ['RUPTURE DE STOCK'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.nvidia.com/fr-fr/geforce/graphics-cards/rtx-2060-super/', 21 | }, 22 | { 23 | brand: 'nvidia', 24 | model: 'founders edition', 25 | series: '3060ti', 26 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203060%20Ti&manufacturer=NVIDIA', 27 | }, 28 | { 29 | brand: 'nvidia', 30 | model: 'founders edition', 31 | series: '3070', 32 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203070&manufacturer=NVIDIA', 33 | }, 34 | { 35 | brand: 'nvidia', 36 | model: 'founders edition', 37 | series: '3070ti', 38 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203070%20Ti&manufacturer=NVIDIA', 39 | }, 40 | { 41 | brand: 'nvidia', 42 | model: 'founders edition', 43 | series: '3080', 44 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203080&manufacturer=NVIDIA', 45 | }, 46 | { 47 | brand: 'nvidia', 48 | model: 'founders edition', 49 | series: '3080ti', 50 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203080%20Ti&manufacturer=NVIDIA', 51 | }, 52 | { 53 | brand: 'nvidia', 54 | model: 'founders edition', 55 | series: '3090', 56 | url: 'https://shop.nvidia.com/fr-fr/geforce/store/gpu/?page=1&limit=9&locale=fr-fr&category=GPU&gpu=RTX%203090&manufacturer=NVIDIA', 57 | }, 58 | ], 59 | name: 'nvidia-fr', 60 | country: 'FR', 61 | }; 62 | -------------------------------------------------------------------------------- /src/store/model/nvidia-de.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const NvidiaDE: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.buy', 8 | text: ['In den Einkaufswagen', 'JETZT KAUFEN'], 9 | }, 10 | outOfStock: { 11 | container: '.buy', 12 | text: ['DERZEIT NICHT VERFÜGBAR'], 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.nvidia.com/de-de/geforce/graphics-cards/rtx-2060-super/', 21 | }, 22 | { 23 | brand: 'nvidia', 24 | model: 'founders edition', 25 | series: '3060ti', 26 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203060%20Ti&manufacturer=NVIDIA', 27 | }, 28 | { 29 | brand: 'nvidia', 30 | model: 'founders edition', 31 | series: '3070', 32 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203070&manufacturer=NVIDIA', 33 | }, 34 | { 35 | brand: 'nvidia', 36 | model: 'founders edition', 37 | series: '3070ti', 38 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203070%20Ti&manufacturer=NVIDIA', 39 | }, 40 | { 41 | brand: 'nvidia', 42 | model: 'founders edition', 43 | series: '3080', 44 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203080&manufacturer=NVIDIA', 45 | }, 46 | { 47 | brand: 'nvidia', 48 | model: 'founders edition', 49 | series: '3080ti', 50 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203080%20Ti&manufacturer=NVIDIA', 51 | }, 52 | { 53 | brand: 'nvidia', 54 | model: 'founders edition', 55 | series: '3090', 56 | url: 'https://shop.nvidia.com/de-de/geforce/store/gpu/?page=1&limit=9&locale=de-de&category=GPU&gpu=RTX%203090&manufacturer=NVIDIA', 57 | }, 58 | ], 59 | name: 'nvidia-de', 60 | country: 'DE', 61 | }; 62 | -------------------------------------------------------------------------------- /src/store/model/amd-it.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmdIt: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.btn-shopping-cart', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '.product-page-description h4', 12 | euroFormat: true, 13 | }, 14 | }, 15 | links: [ 16 | { 17 | brand: 'test:brand', 18 | model: 'test:model', 19 | series: 'test:series', 20 | url: 'https://www.amd.com/en/direct-buy/5450881400/it', 21 | }, 22 | { 23 | brand: 'amd', 24 | cartUrl: 25 | 'https://www.amd.com/en/direct-buy/5450881400/it?add-to-cart=true', 26 | model: '5950x', 27 | series: 'ryzen5950', 28 | url: 'https://www.amd.com/en/direct-buy/5450881400/it', 29 | }, 30 | { 31 | brand: 'amd', 32 | cartUrl: 33 | 'https://www.amd.com/en/direct-buy/5450881500/it?add-to-cart=true', 34 | model: '5900x', 35 | series: 'ryzen5900', 36 | url: 'https://www.amd.com/en/direct-buy/5450881500/it', 37 | }, 38 | { 39 | brand: 'amd', 40 | cartUrl: 41 | 'https://www.amd.com/en/direct-buy/5450881600/it?add-to-cart=true', 42 | model: '5800x', 43 | series: 'ryzen5800', 44 | url: 'https://www.amd.com/en/direct-buy/5450881600/it', 45 | }, 46 | { 47 | brand: 'amd', 48 | cartUrl: 49 | 'https://www.amd.com/en/direct-buy/5450881700/it?add-to-cart=true', 50 | model: '5600x', 51 | series: 'ryzen5600', 52 | url: 'https://www.amd.com/en/direct-buy/5450881700/it', 53 | }, 54 | { 55 | brand: 'amd', 56 | cartUrl: 57 | 'https://www.amd.com/en/direct-buy/5458374100/it?add-to-cart=true', 58 | model: 'amd reference', 59 | series: 'rx6800xt', 60 | url: 'https://www.amd.com/en/direct-buy/5458374100/it', 61 | }, 62 | { 63 | brand: 'amd', 64 | cartUrl: 65 | 'https://www.amd.com/en/direct-buy/5496921500/it?add-to-cart=true', 66 | model: 'amd reference', 67 | series: 'rx6800xt', 68 | url: 'https://www.amd.com/en/direct-buy/5496921500/it', 69 | }, 70 | ], 71 | name: 'amd-it', 72 | country: 'IT', 73 | }; 74 | -------------------------------------------------------------------------------- /src/store/model/dustinhome-no.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const DustinHomeNO: Store = { 4 | currency: 'kr.', 5 | labels: { 6 | inStock: { 7 | container: 8 | 'div.c-product-main-info > div.c-product-buy-wrapper > div.u-pt-16.u-relative.d-flex > div > span', 9 | text: ['Kan sendes omgående'], 10 | }, 11 | outOfStock: { 12 | container: 13 | 'div.c-product-main-info > div.c-product-buy-wrapper > div.u-pt-16.u-relative.d-flex > div > span', 14 | text: [ 15 | 'Vi venter produktet til lager, men har foreløpig ingen bekreftet leveringsdato. Vi sender produktet så snart det er på lager.', 16 | ], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.dustinhome.no/product/5011216501/', 25 | }, 26 | { 27 | brand: 'msi', 28 | model: 'suprim x', 29 | series: '3080', 30 | url: 'https://www.dustinhome.no/product/5011216707/', 31 | }, 32 | { 33 | brand: 'evga', 34 | model: 'ftw3 ultra', 35 | series: '3080', 36 | url: 'https://www.dustinhome.no/product/5011197759/', 37 | }, 38 | { 39 | brand: 'evga', 40 | model: 'ftw3', 41 | series: '3080', 42 | url: 'https://www.dustinhome.no/product/5011197760/', 43 | }, 44 | { 45 | brand: 'evga', 46 | model: 'xc3 ultra', 47 | series: '3080', 48 | url: 'https://www.dustinhome.no/product/5011197756/', 49 | }, 50 | { 51 | brand: 'pny', 52 | model: 'xlr8 epic x', 53 | series: '3080', 54 | url: 'https://www.dustinhome.no/product/5011196134/', 55 | }, 56 | { 57 | brand: 'pny', 58 | model: 'xlr8 epic x', 59 | series: '3080', 60 | url: 'https://www.dustinhome.no/product/5011196133/', 61 | }, 62 | { 63 | brand: 'gigabyte', 64 | model: 'aorus xtreme waterforce wb', 65 | series: '3080', 66 | url: 'https://www.dustinhome.no/product/5011212484/', 67 | }, 68 | { 69 | brand: 'gigabyte', 70 | model: 'aorus master', 71 | series: '3080', 72 | url: 'https://www.dustinhome.no/product/5011199977/', 73 | }, 74 | ], 75 | name: 'dustinhome-no', 76 | country: 'NO', 77 | }; 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Create release 9 | runs-on: ubuntu-latest 10 | outputs: 11 | release_created: ${{ steps.release.outputs.release_created }} 12 | tag_name: ${{ steps.release.outputs.tag_name }} 13 | tag_name_no_v: ${{ steps.normalize_tag.outputs.tag_name_no_v }} 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Run release-please 18 | uses: google-github-actions/release-please-action@v3 19 | id: release 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | release-type: node 23 | changelog-path: CHANGELOG.md 24 | package-name: streetmerchant 25 | changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false},{"type":"ci","section":"Continuous Integration","hidden":false},{"type":"docs","section":"Documentation","hidden":false},{"type":"refactor","section":"Refactoring","hidden":false},{"type":"perf","section":"Performance","hidden":false},{"type":"test","section":"Tests","hidden":false}]' 26 | - name: Normalize tag name 27 | if: steps.release.outputs.release_created == 'true' 28 | id: normalize_tag 29 | run: | 30 | tag=${{ steps.release.outputs.tag_name }} 31 | echo "tag_name_no_v=${tag#v}" >> "$GITHUB_OUTPUT" 32 | build_tag_publish: 33 | name: Build, tag, and publish 34 | runs-on: ubuntu-latest 35 | needs: release 36 | if: needs.release.outputs.release_created == 'true' 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | - name: Login to GitHub Container Registry 41 | uses: docker/login-action@v2 42 | with: 43 | registry: ghcr.io 44 | username: ${{ github.actor }} 45 | password: ${{ secrets.GITHUB_TOKEN }} 46 | - name: Build and push Docker image 47 | uses: docker/build-push-action@v3 48 | with: 49 | context: . 50 | push: true 51 | tags: "ghcr.io/jef/streetmerchant:latest,ghcr.io/jef/streetmerchant:${{ needs.release.outputs.tag_name_no_v }}" 52 | -------------------------------------------------------------------------------- /src/store/model/aria.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import {getProductLinksBuilder} from './helpers/card'; 3 | 4 | export const Aria: Store = { 5 | currency: '£', 6 | labels: { 7 | inStock: { 8 | container: '#addQuantity', 9 | text: ['add to shopping basket'], 10 | }, 11 | maxPrice: { 12 | container: '.priceBig', 13 | euroFormat: false, // Note: Aria uses non-euroFromat as price seperator 14 | }, 15 | outOfStock: { 16 | container: '.fBox', 17 | text: ['out of stock', 'there is currently no stock of this item'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'test:brand', 23 | model: 'test:model', 24 | series: 'test:series', 25 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+2060+Super/Gigabyte+NVIDIA+GeForce+RTX+2060+SUPER+8GB+WINDFORCE+OC+Turing+Graphics+Card+%2B+RTX+Bundle%21?productId=71541', 26 | }, 27 | { 28 | brand: 'amd', 29 | model: '5950x', 30 | series: 'ryzen5950', 31 | url: 'https://www.aria.co.uk/Products/Components/Processors/AMD+CPUs/Ryzen+9+-+Socket+AM4/AMD+Ryzen+9+5950X+16+Core+AM4+CPU%2FProcessor?productId=72868', 32 | }, 33 | ], 34 | linksBuilder: { 35 | builder: getProductLinksBuilder({ 36 | productsSelector: '#productListingInner .listTable .listTableTr', 37 | sitePrefix: 'https://www.aria.co.uk', 38 | titleSelector: 'strong > a[href]', 39 | }), 40 | urls: [ 41 | { 42 | series: '3060', 43 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+3060', 44 | }, 45 | { 46 | series: '3060ti', 47 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+3060+Ti', 48 | }, 49 | { 50 | series: '3070', 51 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+3070', 52 | }, 53 | { 54 | series: '3080', 55 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+3080', 56 | }, 57 | { 58 | series: '3090', 59 | url: 'https://www.aria.co.uk/Products/Components/Graphics+Cards/NVIDIA+GeForce/GeForce+RTX+3090', 60 | }, 61 | ], 62 | }, 63 | name: 'aria', 64 | country: 'UK', 65 | waitUntil: 'domcontentloaded', 66 | }; 67 | -------------------------------------------------------------------------------- /src/messaging/notification.ts: -------------------------------------------------------------------------------- 1 | import {DMPayload} from '.'; 2 | import {Link, Store} from '../store/model'; 3 | import {sendApns} from './apns'; 4 | import {sendDesktopNotification} from './desktop'; 5 | import {sendDMAsync as sendDiscordDM, sendDiscordMessage} from './discord'; 6 | import {sendEmail} from './email'; 7 | import {sendFreeMobileAlert} from './freemobile'; 8 | import {sendGotifyNotification} from './gotify'; 9 | import {sendMqttMessage} from './mqtt'; 10 | import {sendNtfyAlert} from './ntfy'; 11 | import {sendPagerDutyNotification} from './pagerduty'; 12 | import {adjustPhilipsHueLights} from './philips-hue'; 13 | import {sendPushbulletNotification} from './pushbullet'; 14 | import {sendPushoverNotification} from './pushover'; 15 | import {updateRedis} from './redis'; 16 | import {sendDMAsync as sendSlackDM, sendSlackMessage} from './slack'; 17 | import {sendSms} from './sms'; 18 | import {playSound} from './sound'; 19 | import {sendStreamLabsAlert} from './streamlabs'; 20 | import {sendTelegramMessage} from './telegram'; 21 | import {sendTwilioMessage} from './twilio'; 22 | // import {sendTwitchMessage} from './twitch'; 23 | import {sendTweet} from './twitter'; 24 | 25 | export function sendNotification(link: Link, store: Store) { 26 | // Priority 27 | playSound(); 28 | sendNtfyAlert(link, store); 29 | sendDiscordMessage(link, store); 30 | sendDesktopNotification(link, store); 31 | sendEmail(link, store); 32 | sendSms(link, store); 33 | sendApns(link, store); 34 | // Non-priority 35 | adjustPhilipsHueLights(); 36 | sendGotifyNotification(link, store); 37 | sendMqttMessage(link, store); 38 | sendPagerDutyNotification(link, store); 39 | sendPushbulletNotification(link, store); 40 | sendPushoverNotification(link, store); 41 | sendSlackMessage(link, store); 42 | sendTelegramMessage(link, store); 43 | sendTweet(link, store); 44 | sendTwilioMessage(link, store); 45 | // sendTwitchMessage(link, store); 46 | updateRedis(link, store); 47 | sendStreamLabsAlert(link, store); 48 | sendFreeMobileAlert(link, store); 49 | } 50 | 51 | export async function sendDMAsync(service: string, payload: DMPayload) { 52 | let dmFunction = undefined; 53 | switch (service) { 54 | case 'slack': 55 | dmFunction = sendSlackDM; 56 | break; 57 | case 'discord': 58 | dmFunction = sendDiscordDM; 59 | break; 60 | default: 61 | dmFunction = () => void 0; 62 | } 63 | await dmFunction(payload); 64 | } 65 | -------------------------------------------------------------------------------- /src/store/model/pbtech.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const PBTech: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: [ 8 | { 9 | container: 10 | '#product_right_column > div.product_bgWrap > div.p_ov_stock_summary_dd > div > div', 11 | text: ['In stock'], 12 | }, 13 | { 14 | container: '.p_stock_title_dd', 15 | text: ['In stock'], 16 | }, 17 | ], 18 | maxPrice: { 19 | container: 'div.p_price_dd > div.p_price > span.ginc', 20 | euroFormat: false, 21 | }, 22 | outOfStock: { 23 | container: 24 | '#product_right_column > div.product_bgWrap > div.p_ov_stock_summary_dd > div > div', 25 | text: ['Available on order', 'Unavailable', 'Out of stock'], 26 | }, 27 | }, 28 | links: [ 29 | { 30 | brand: 'asus', 31 | model: 'strix', 32 | series: '3080', 33 | url: 'https://www.pbtech.com/au/product/VGAAS33087/ASUS-ROG-STRIX-GeForce-RTX-3080-10GB-GDDR6X-PCIE-4', 34 | }, 35 | { 36 | brand: 'asus', 37 | model: 'strix oc white', 38 | series: '3080', 39 | url: 'https://www.pbtech.com/au/product/VGAAS33086/ASUS-ROG-STRIX-GeForce-RTX-3080-O10G-Gaming-WHITE', 40 | }, 41 | { 42 | brand: 'sapphire', 43 | model: 'nitro+', 44 | series: 'rx6900xt', 45 | url: 'https://www.pbtech.com/au/product/VGASAP16915/Sapphire-Nitro-AMD-Radeon-RX-6900-XT-OC-Gaming-Gra', 46 | }, 47 | { 48 | brand: 'asrock', 49 | model: 'taichi', 50 | series: 'rx6800xt', 51 | url: 'https://www.pbtech.com/au/product/VGAASR06810/ASRock-Radeon-RX-6800-XT-Taichi-X-OC-Graphics-Card', 52 | }, 53 | { 54 | brand: 'asrock', 55 | model: 'challenger pro', 56 | series: 'rx6800', 57 | url: 'https://www.pbtech.com/au/product/VGAASR06800/ASRock-Radeon-RX-6800-Challenger-Pro-Graphics-Card', 58 | }, 59 | { 60 | brand: 'msi', 61 | model: 'gaming x trio', 62 | series: 'rx6800', 63 | url: 'https://www.pbtech.com/au/product/VGAMSI66801/MSI-Radeon-RX-6800-Gaming-X-TRIO-16GB-GDDR6-PCIE-4', 64 | }, 65 | { 66 | brand: 'sapphire', 67 | model: 'pulse', 68 | series: 'rx6800', 69 | url: 'https://www.pbtech.com/au/product/VGASAP16802/Sapphire-PULSE-AMD-Radeon-RX-6800-OC-Graphics-Card', 70 | }, 71 | ], 72 | name: 'pbtech', 73 | country: 'AU', 74 | }; 75 | -------------------------------------------------------------------------------- /src/store/model/eprice.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Eprice: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.topSideDx', 8 | text: ['disponibile', 'pochi pezzi'], 9 | }, 10 | maxPrice: { 11 | container: '#PrezzoClasic span[class*="big"]', 12 | euroFormat: true, 13 | }, 14 | outOfStock: { 15 | container: '.dispo', 16 | text: ['ESAURITO O FUORI PROD.'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.eprice.it/cuffie-con-microfono-APPLE/d-9030906', 25 | }, 26 | { 27 | brand: 'msi', 28 | model: 'ventus 3x oc', 29 | series: '3070', 30 | url: 'https://www.eprice.it/schede-video-MSI/d-14039974', 31 | }, 32 | { 33 | brand: 'asus', 34 | model: 'dual', 35 | series: '3070', 36 | url: 'https://www.eprice.it/schede-video-ASUS/d-14042082', 37 | }, 38 | { 39 | brand: 'asus', 40 | model: 'strix', 41 | series: '3070', 42 | url: 'https://www.eprice.it/schede-video-ASUS/d-14039878', 43 | }, 44 | { 45 | brand: 'asus', 46 | model: 'tuf', 47 | series: '3070', 48 | url: 'https://www.eprice.it/schede-video-ASUS/d-14039876', 49 | }, 50 | { 51 | brand: 'msi', 52 | model: 'gaming', 53 | series: '3070', 54 | url: 'https://www.eprice.it/schede-video-MSI/d-14039972', 55 | }, 56 | { 57 | brand: 'msi', 58 | model: 'ventus 3x oc', 59 | series: '3070', 60 | url: 'https://www.eprice.it/schede-video-MSI/d-14039974', 61 | }, 62 | { 63 | brand: 'msi', 64 | model: 'ventus 2x oc', 65 | series: '3070', 66 | url: 'https://www.eprice.it/schede-video-MSI/d-14039973', 67 | }, 68 | { 69 | brand: 'zotac', 70 | model: 'gaming', 71 | series: '3070', 72 | url: 'https://www.eprice.it/schede-video-ZOTAC/d-13979806', 73 | }, 74 | { 75 | brand: 'sony', 76 | model: 'ps5 console', 77 | series: 'sonyps5c', 78 | url: 'https://www.eprice.it/playstation-5-SONY/d-13981612', 79 | }, 80 | { 81 | brand: 'sony', 82 | model: 'ps5 digital', 83 | series: 'sonyps5de', 84 | url: 'https://www.eprice.it/playstation-5-SONY/d-13981613', 85 | }, 86 | ], 87 | name: 'eprice', 88 | country: 'IT', 89 | }; 90 | -------------------------------------------------------------------------------- /src/store/fetch-links.ts: -------------------------------------------------------------------------------- 1 | import {Link, Series, Store} from './model'; 2 | import {Print, logger} from '../logger'; 3 | import {Browser} from 'puppeteer'; 4 | import cheerio from 'cheerio'; 5 | import {filterSeries} from './filter'; 6 | import {usingPage} from '../util'; 7 | 8 | function addNewLinks(store: Store, links: Link[], series: Series) { 9 | if (links.length === 0) { 10 | logger.debug(Print.message('NO STORE LINKS FOUND', series, store, true)); 11 | 12 | return; 13 | } 14 | 15 | const existingUrls = new Set(store.links.map(link => link.url)); 16 | const newLinks = links.filter(link => !existingUrls.has(link.url)); 17 | 18 | if (newLinks.length === 0) { 19 | logger.debug(Print.message('NO NEW LINKS FOUND', series, store, true)); 20 | return; 21 | } 22 | 23 | logger.debug( 24 | Print.message(`FOUND ${newLinks.length} NEW LINKS`, series, store, true) 25 | ); 26 | logger.debug(JSON.stringify(newLinks, null, 2)); 27 | 28 | store.links = store.links.concat(newLinks); 29 | } 30 | 31 | export async function fetchLinks(store: Store, browser: Browser) { 32 | const linksBuilder = store.linksBuilder; 33 | if (!linksBuilder) { 34 | return; 35 | } 36 | 37 | const promises: Array> = []; 38 | 39 | // eslint-disable-next-line prefer-const 40 | for (let {series, url} of linksBuilder.urls) { 41 | if (!filterSeries(series)) { 42 | continue; 43 | } 44 | 45 | logger.debug(Print.message('DETECTING STORE LINKS', series, store, true)); 46 | 47 | if (!Array.isArray(url)) { 48 | url = [url]; 49 | } 50 | 51 | url.map(x => 52 | promises.push( 53 | usingPage(browser, async page => { 54 | const waitUntil = linksBuilder.waitUntil 55 | ? linksBuilder.waitUntil 56 | : 'domcontentloaded'; 57 | await page.goto(x, {waitUntil}); 58 | 59 | if (linksBuilder.waitForSelector) { 60 | await page.waitForSelector(linksBuilder.waitForSelector); 61 | } 62 | 63 | const html = await page.content(); 64 | 65 | if (!html) { 66 | logger.error(Print.message('NO RESPONSE', series, store, true)); 67 | return; 68 | } 69 | 70 | const docElement = cheerio.load(html).root(); 71 | const links = linksBuilder.builder(docElement, series); 72 | 73 | addNewLinks(store, links, series); 74 | }) 75 | ) 76 | ); 77 | } 78 | 79 | await Promise.all(promises); 80 | } 81 | -------------------------------------------------------------------------------- /src/store/model/centrecom.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Centrecom: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: '.prod_stores_stock > li:nth-child(1) > span:nth-child(2)', 9 | text: ['In Stock'], 10 | }, 11 | maxPrice: { 12 | container: 'div.prod_price_current.product-price > span', 13 | euroFormat: false, 14 | }, 15 | outOfStock: { 16 | container: '.prod_stores_stock > li:nth-child(1) > span:nth-child(2)', 17 | text: ['Sold Out'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'asus', 23 | model: 'tuf oc', 24 | series: '3080', 25 | url: 'https://www.centrecom.com.au/asus-geforce-rtx-3080-tuf-oc-10gb-gaming-graphics-card', 26 | }, 27 | { 28 | brand: 'galax', 29 | model: 'sg oc', 30 | series: '3080', 31 | url: 'https://www.centrecom.com.au/galax-geforce-rtx-3080-sg-1-click-oc-graphics-card', 32 | }, 33 | { 34 | brand: 'gigabyte', 35 | model: 'aorus master', 36 | series: '3080', 37 | url: 'https://www.centrecom.com.au/gigabyte-aorus-geforce-rtx-3080-master-graphics-card', 38 | }, 39 | { 40 | brand: 'gigabyte', 41 | model: 'aorus xtreme', 42 | series: '3080', 43 | url: 'https://www.centrecom.com.au/gigabyte-geforce-rtx-3080-aorus-extreme-10gb-gddr6x-graphics-card', 44 | }, 45 | { 46 | brand: 'gigabyte', 47 | model: 'eagle oc', 48 | series: '3080', 49 | url: 'https://www.centrecom.com.au/gigabyte-geforce-rtx-3080-eagle-oc-10g-graphics-card', 50 | }, 51 | { 52 | brand: 'gigabyte', 53 | model: 'gaming oc', 54 | series: '3080', 55 | url: 'https://www.centrecom.com.au/gigabyte-geforce-rtx-3080-gaming-oc-10g-graphics-card', 56 | }, 57 | { 58 | brand: 'msi', 59 | model: 'gaming x trio', 60 | series: '3080', 61 | url: 'https://www.centrecom.com.au/msi-geforce-rtx-3080-gaming-x-trio-10g-graphics-card', 62 | }, 63 | { 64 | brand: 'msi', 65 | model: 'suprim x', 66 | series: '3080', 67 | url: 'https://www.centrecom.com.au/msi-geforce-rtx-3080-suprim-x-10g-graphics-card', 68 | }, 69 | { 70 | brand: 'msi', 71 | model: 'ventus 3x oc', 72 | series: '3080', 73 | url: 'https://www.centrecom.com.au/msi-geforce-rtx-3080-ventus-3x-oc-10gb-graphics-card', 74 | }, 75 | ], 76 | name: 'centrecom', 77 | country: 'AU', 78 | }; 79 | -------------------------------------------------------------------------------- /src/messaging/sms.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from '../store/model'; 2 | import {Print, logger} from '../logger'; 3 | import Mail from 'nodemailer/lib/mailer'; 4 | import {config} from '../config'; 5 | import {transporter} from './email'; 6 | 7 | const {email, phone} = config.notifications; 8 | 9 | if (phone.number.length > 0 && (!email.username || !email.password)) { 10 | logger.warn( 11 | '✖ in order to receive sms alerts, email notifications must also be configured' 12 | ); 13 | } 14 | 15 | if (phone.carrier.length !== phone.number.length) { 16 | logger.warn( 17 | '✖ the number of carriers must match the number of phone numbers', 18 | {carrier: phone.carrier, number: phone.number} 19 | ); 20 | } 21 | 22 | export function sendSms(link: Link, store: Store) { 23 | for ( 24 | let i = 0; 25 | i < Math.max(phone.number.length, phone.carrier.length); 26 | i++ 27 | ) { 28 | const currentNumber = phone.number[i]; 29 | const currentCarrier = phone.carrier[i]; 30 | 31 | if (!currentNumber) { 32 | logger.error(`✖ ${currentCarrier} is not associated with a number`); 33 | continue; 34 | } else if (!currentCarrier) { 35 | logger.error(`✖ ${currentNumber} is not associated with a carrier`); 36 | continue; 37 | } 38 | 39 | if (!phone.availableCarriers.has(currentCarrier)) { 40 | logger.error(`✖ unknown carrier ${currentCarrier}`); 41 | continue; 42 | } 43 | 44 | logger.debug('↗ sending sms'); 45 | 46 | const mailOptions: Mail.Options = { 47 | attachments: link.screenshot 48 | ? [ 49 | { 50 | filename: link.screenshot, 51 | path: `./${link.screenshot}`, 52 | }, 53 | ] 54 | : undefined, 55 | from: email.username, 56 | subject: Print.inStock(link, store, false, true), 57 | text: link.cartUrl ? link.cartUrl : link.url, 58 | to: generateAddress(currentNumber, currentCarrier), 59 | }; 60 | 61 | transporter.sendMail(mailOptions, error => { 62 | if (error) { 63 | logger.error( 64 | `✖ couldn't send sms to ${currentNumber} for carrier ${currentCarrier}`, 65 | error 66 | ); 67 | } else { 68 | logger.info('✔ sms sent'); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | function generateAddress(number: string, carrier: string): string { 75 | if (carrier && phone.availableCarriers.has(carrier)) { 76 | return [number, phone.availableCarriers.get(carrier)].join('@'); 77 | } 78 | 79 | return ''; 80 | } 81 | -------------------------------------------------------------------------------- /src/store/model/amd-nl.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AmdNl: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.btn-shopping-cart', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: '.product-page-description h4', 12 | euroFormat: true, 13 | }, 14 | outOfStock: { 15 | container: '.btn-radeon', 16 | text: ['out of stock'], 17 | }, 18 | }, 19 | links: [ 20 | { 21 | brand: 'test:brand', 22 | model: 'test:model', 23 | series: 'test:series', 24 | url: 'https://www.amd.com/en/direct-buy/5450881400/nl', 25 | }, 26 | { 27 | brand: 'amd', 28 | cartUrl: 29 | 'https://www.amd.com/en/direct-buy/5450881400/nl?add-to-cart=true', 30 | model: '5950x', 31 | series: 'ryzen5950', 32 | url: 'https://www.amd.com/en/direct-buy/5450881400/nl', 33 | }, 34 | { 35 | brand: 'amd', 36 | cartUrl: 37 | 'https://www.amd.com/en/direct-buy/5450881500/nl?add-to-cart=true', 38 | model: '5900x', 39 | series: 'ryzen5900', 40 | url: 'https://www.amd.com/en/direct-buy/5450881500/nl', 41 | }, 42 | { 43 | brand: 'amd', 44 | cartUrl: 45 | 'https://www.amd.com/en/direct-buy/5450881600/nl?add-to-cart=true', 46 | model: '5800x', 47 | series: 'ryzen5800', 48 | url: 'https://www.amd.com/en/direct-buy/5450881600/nl', 49 | }, 50 | { 51 | brand: 'amd', 52 | cartUrl: 53 | 'https://www.amd.com/en/direct-buy/5450881700/nl?add-to-cart=true', 54 | model: '5600x', 55 | series: 'ryzen5600', 56 | url: 'https://www.amd.com/en/direct-buy/5450881700/nl', 57 | }, 58 | { 59 | brand: 'amd', 60 | cartUrl: 61 | 'https://www.amd.com/en/direct-buy/5458374000/nl?add-to-cart=true', 62 | model: 'amd reference', 63 | series: 'rx6800', 64 | url: 'https://www.amd.com/en/direct-buy/5458374000/nl', 65 | }, 66 | { 67 | brand: 'amd', 68 | cartUrl: 69 | 'https://www.amd.com/en/direct-buy/5458374100/nl?add-to-cart=true', 70 | model: 'amd reference', 71 | series: 'rx6800xt', 72 | url: 'https://www.amd.com/en/direct-buy/5458374100/nl', 73 | }, 74 | { 75 | brand: 'amd', 76 | cartUrl: 77 | 'https://www.amd.com/en/direct-buy/5458374200/nl?add-to-cart=true', 78 | model: 'amd reference', 79 | series: 'rx6900xt', 80 | url: 'https://www.amd.com/en/direct-buy/5458374200/nl', 81 | }, 82 | ], 83 | name: 'amd-nl', 84 | country: 'NL', 85 | }; 86 | -------------------------------------------------------------------------------- /src/store/model/mediamarkt-at.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const MediamarktAt: Store = { 4 | backoffStatusCodes: [403, 429, 503], 5 | currency: '€', 6 | labels: { 7 | captcha: { 8 | container: 'p', 9 | text: ['Das ging uns leider zu schnell.'], 10 | }, 11 | maxPrice: { 12 | container: 'span[font-family="price"]', 13 | euroFormat: false, 14 | }, 15 | outOfStock: [ 16 | { 17 | container: '#root', 18 | text: ['Dieser Artikel ist aktuell nicht verfügbar.'], 19 | }, 20 | { 21 | container: '#root', 22 | text: ['Leider keine Lieferung möglich'], 23 | }, 24 | { 25 | container: '#root', 26 | text: ['Nicht verfügbar'], 27 | }, 28 | { 29 | container: '#root', 30 | text: ['Dieser Artikel ist dauerhaft ausverkauft'], 31 | }, 32 | { 33 | container: '#root', 34 | text: ['Dieser Artikel ist bald wieder für Sie verfügbar'], 35 | }, 36 | { 37 | container: '#root', 38 | text: ['Dieser Artikel ist bald wieder verfügbar'], 39 | }, 40 | ], 41 | }, 42 | links: [ 43 | { 44 | brand: 'test:brand', 45 | model: 'test:model', 46 | series: 'test:series', 47 | url: 'https://www.mediamarkt.at/de/product/-1759580.html', 48 | }, 49 | { 50 | brand: 'gainward', 51 | model: 'phoenix', 52 | series: '3060ti', 53 | url: 'https://www.mediamarkt.at/de/product/-1815563.html', 54 | }, 55 | { 56 | brand: 'gainward', 57 | model: 'phantom gaming', 58 | series: '3080', 59 | url: 'https://www.mediamarkt.at/de/product/-1817678.html', 60 | }, 61 | { 62 | brand: 'asus', 63 | model: 'dual', 64 | series: '3060ti', 65 | url: 'https://www.mediamarkt.at/de/product/-1812392.html', 66 | }, 67 | { 68 | brand: 'zotac', 69 | model: 'trinity', 70 | series: '3080', 71 | url: 'https://www.mediamarkt.at/de/product/-1803318.html', 72 | }, 73 | { 74 | brand: 'asus', 75 | model: 'tuf', 76 | series: '3080', 77 | url: 'https://www.mediamarkt.at/de/product/-1799192.html', 78 | }, 79 | { 80 | brand: 'msi', 81 | model: 'ventus 2x', 82 | series: '3070', 83 | url: 'https://www.mediamarkt.at/de/product/-1812232.html', 84 | }, 85 | { 86 | brand: 'msi', 87 | model: 'gaming x trio', 88 | series: '3070', 89 | url: 'https://www.mediamarkt.at/de/product/-1812223.html', 90 | }, 91 | ], 92 | name: 'mediamarkt-at', 93 | country: 'AT', 94 | }; 95 | -------------------------------------------------------------------------------- /src/store/model/wellstechnology.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const WellsTechnology: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: 9 | '#addToCartForm-product-template > div.payment-buttons.payment-buttons--small > div > div > div > div', 10 | text: ['Buy it now'], 11 | }, 12 | maxPrice: { 13 | container: '#productPrice-product-template *', 14 | euroFormat: false, 15 | }, 16 | outOfStock: { 17 | container: '#addToCartText-product-template', 18 | text: ['Sold Out'], 19 | }, 20 | }, 21 | links: [ 22 | { 23 | brand: 'evga', 24 | model: 'xc3', 25 | series: '3080', 26 | url: 'https://wellstechnology.com.au/products/evga-geforce-rtx3080-xc3-10gb-gddr6x?_pos=1&_sid=bcf224e44&_ss=r&variant=36272043983014', 27 | }, 28 | { 29 | brand: 'evga', 30 | model: 'ftw3 ultra', 31 | series: '3080', 32 | url: 'https://wellstechnology.com.au/products/evga-geforce-rtx3080-ftw3-ultra-10gb-gddr6x?_pos=2&_sid=bcf224e44&_ss=r&variant=36271898919078', 33 | }, 34 | { 35 | brand: 'gigabyte', 36 | model: 'gaming oc', 37 | series: '3080', 38 | url: 'https://wellstechnology.com.au/products/gigabyte-n3080gaming-oc-10gd-rtx3080-video-card?_pos=3&_sid=bcf224e44&_ss=r&variant=36210887295142', 39 | }, 40 | { 41 | brand: 'gigabyte', 42 | model: 'aorus xtreme waterforce wb', 43 | series: '3080', 44 | url: 'https://wellstechnology.com.au/products/gigabyte-aorus-rtx3080-xtreme-waterforce-wb?_pos=1&_sid=fd83b064b&_ss=r', 45 | }, 46 | { 47 | brand: 'amd', 48 | model: '5900x', 49 | series: 'ryzen5900', 50 | url: 'https://wellstechnology.com.au/products/amd-ryzen-9-5950x-cpu?_pos=1&_sid=cc7b6903f&_ss=r&variant=37019002339494', 51 | }, 52 | { 53 | brand: 'amd', 54 | model: '5900x', 55 | series: 'ryzen5900', 56 | url: 'https://wellstechnology.com.au/products/amd-ryzen-5-5900x-cpu?_pos=1&_sid=b9234b72d&_ss=r&variant=36941124337830', 57 | }, 58 | { 59 | brand: 'amd', 60 | model: '5800x', 61 | series: 'ryzen5800', 62 | url: 'https://wellstechnology.com.au/products/amd-ryzen-5-5800x-cpu?_pos=1&_sid=35b306d65&_ss=r&variant=36941094387878', 63 | }, 64 | { 65 | brand: 'amd', 66 | model: '5600x', 67 | series: 'ryzen5600', 68 | url: 'https://wellstechnology.com.au/products/amd-ryzen-5-5600x-cpu?_pos=1&_sid=3f4c61e03&_ss=r&variant=36941063422118', 69 | }, 70 | ], 71 | name: 'wellstechnology', 72 | country: 'AU', 73 | }; 74 | -------------------------------------------------------------------------------- /src/store/model/walmart.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Walmart: Store = { 4 | currency: '$', 5 | labels: { 6 | inStock: { 7 | container: '.button.spin-button.prod-ProductCTA--primary.button--primary', 8 | text: ['add to cart'], 9 | }, 10 | maxPrice: { 11 | container: 'span[class*="price-characteristic"]', 12 | }, 13 | }, 14 | links: [ 15 | { 16 | brand: 'test:brand', 17 | model: 'test:model', 18 | series: 'test:series', 19 | url: 'https://www.walmart.com/ip/Keurig-K-compact-Brewer-Black-Coffee-Maker/806217614', 20 | }, 21 | { 22 | brand: 'sony', 23 | model: 'ps5 console', 24 | series: 'sonyps5c', 25 | url: 'https://www.walmart.com/ip/PlayStation5-Console/363472942', 26 | }, 27 | { 28 | brand: 'sony', 29 | model: 'ps5 digital', 30 | series: 'sonyps5de', 31 | url: 'https://www.walmart.com/ip/Sony-PlayStation-5-Digital-Edition/493824815', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series x', 36 | series: 'xboxsx', 37 | url: 'https://www.walmart.com/ip/Xbox-Series-X/443574645', 38 | }, 39 | { 40 | brand: 'microsoft', 41 | model: 'xbox series s', 42 | series: 'xboxss', 43 | url: 'https://www.walmart.com/ip/Xbox-Series-S/606518560', 44 | }, 45 | { 46 | brand: 'corsair', 47 | model: '750 platinum', 48 | series: 'sf', 49 | url: 'https://www.walmart.com/ip/SF750-Power-Supply/197046151', 50 | }, 51 | { 52 | brand: 'corsair', 53 | model: '600 platinum', 54 | series: 'sf', 55 | url: 'https://www.walmart.com/ip/Corsair-SF-Series-600W-80-Platinum-Power-Supply/250717047', 56 | }, 57 | { 58 | brand: 'amd', 59 | model: '5900x', 60 | series: 'ryzen5900', 61 | url: 'https://www.walmart.com/ip/AMD-Ryzen-9-5900X-12-core-24-thread-Desktop-Processor/647899167', 62 | }, 63 | { 64 | brand: 'evga', 65 | model: 'ftw3 ultra', 66 | series: '3060ti', 67 | url: 'https://www.walmart.com/ip/912221235', 68 | }, 69 | { 70 | brand: 'evga', 71 | model: 'ftw3 ultra', 72 | series: '3070', 73 | url: 'https://www.walmart.com/ip/804934537', 74 | }, 75 | { 76 | brand: 'nintendo', 77 | model: 'switch 2', 78 | series: 'switch2', 79 | url: 'https://www.walmart.com/ip/15949610846', 80 | }, 81 | { 82 | brand: 'nintendo', 83 | model: 'switch 2 bundle', 84 | series: 'switch2', 85 | url: 'https://www.walmart.com/ip/15928868255', 86 | }, 87 | ], 88 | name: 'walmart', 89 | country: 'US', 90 | }; 91 | -------------------------------------------------------------------------------- /src/store/model/ldlc-italy.ts: -------------------------------------------------------------------------------- 1 | import {getProductLinksBuilder} from './helpers/card'; 2 | import {Store} from './store'; 3 | 4 | export const LDLCItaly: Store = { 5 | currency: '€', 6 | labels: { 7 | inStock: { 8 | container: '.stock', 9 | text: ['Disponibile'], 10 | }, 11 | maxPrice: { 12 | container: '.price', 13 | euroFormat: true, 14 | }, 15 | outOfStock: { 16 | container: '.stock', 17 | text: ['Rottura'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'test:brand', 23 | model: 'test:model', 24 | series: 'test:series', 25 | url: 'https://www.ldlc.com/it-it/scheda/PB00098129.html', 26 | }, 27 | { 28 | brand: 'microsoft', 29 | model: 'xbox series x', 30 | series: 'xboxsx', 31 | url: 'https://www.ldlc.com/it-it/scheda/PB00390450.html', 32 | }, 33 | { 34 | brand: 'microsoft', 35 | model: 'xbox series s', 36 | series: 'xboxss', 37 | url: 'https://www.ldlc.com/it-it/scheda/PB00390458.html', 38 | }, 39 | ], 40 | linksBuilder: { 41 | builder: getProductLinksBuilder({ 42 | productsSelector: '.pdt-item', 43 | sitePrefix: 'https://www.ldlc.com', 44 | titleSelector: '.title-3 > a[href]', 45 | }), 46 | urls: [ 47 | { 48 | series: '3060', 49 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5801+fv121-19509.html', 50 | }, 51 | { 52 | series: '3060ti', 53 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5801+fv121-19365.html', 54 | }, 55 | { 56 | series: '3070', 57 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5801+fv121-19184.html', 58 | }, 59 | { 60 | series: '3080', 61 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5801+fv121-19183.html', 62 | }, 63 | { 64 | series: '3090', 65 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5801+fv121-19185.html', 66 | }, 67 | { 68 | series: 'rx6800', 69 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5800+fv121-19339.html', 70 | }, 71 | { 72 | series: 'rx6800xt', 73 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5800+fv121-19340.html', 74 | }, 75 | { 76 | series: 'rx6900xt', 77 | url: 'https://www.ldlc.com/it-it/informatica/componenti/scheda-video/c4684/+fv1026-5800+fv121-19367.html', 78 | }, 79 | ], 80 | }, 81 | name: 'ldlc-it', 82 | country: 'IT', 83 | }; 84 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What's Node.js and how do I install it? 4 | 5 | Visit [their website](https://nodejs.org/en/) and download and install it. Very straight forward. Otherwise, Google more information related to your system needs. 6 | 7 | ## Will this harm my computer? 8 | 9 | No. 10 | 11 | ## Have you gotten a card yet? 12 | 13 | [Sure did!](https://i.imgur.com/59CRzGq.png) 14 | 15 | ## Will I get banned from of the stores? 16 | 17 | Perhaps, but it's the risk we're willing to take! To help minimize this, take a look at [#1050](https://github.com/jef/streetmerchant/issues/1050). 18 | 19 | ## I got a problem and need help 20 | 21 | File an [issue](https://github.com/jef/streetmerchant/issues/new/choose). I'll do my best to get to you. I work a full time job and this is only a hobby of mine. 22 | 23 | ## How do I get the latest code? 24 | 25 | Run the following commands: 26 | 27 | ```shell 28 | git pull origin main 29 | npm install 30 | npm run start 31 | ``` 32 | 33 | If you changed the code at all, this will most likely fail. You can clear out your changes by doing: 34 | 35 | ```shell 36 | git checkout . 37 | git pull origin main 38 | npm install 39 | npm run start 40 | ``` 41 | 42 | You can also to [git-stash](https://git-scm.com/docs/git-stash), but we won't expand on that here. 43 | 44 | 45 | ## Why don't my notifications work? 46 | 47 | There is probably an [issue](https://github.com/jef/streetmerchant/issues?q=is%3Aissue+sort%3Aupdated-desc+sound+is%3Aclosed) that has [already](https://github.com/jef/streetmerchant/issues/182) [been](https://github.com/jef/streetmerchant/issues/116) [resolved](https://github.com/jef/streetmerchant/issues/155). 48 | 49 | ## I'd love to contribute, how do I do that? 50 | 51 | Make a [pull request](https://github.com/jef/streetmerchant/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc)! All contributions are welcome. 52 | 53 | ## How do I add a store? 54 | 55 | Here's a little write-up I did: [Adding a store](help/general.md#adding-a-store). 56 | 57 | ## Why do I have to download all this stuff just to get this bot working? 58 | 59 | Well, I would rather you didn't either. See [#11](https://github.com/jef/streetmerchant/issues/11). 60 | 61 | ## Why does Amazon show an error page (with a picture of an animal) instead of adding to cart or going to the detail page? 62 | 63 | This is intended; see [#733](https://github.com/jef/streetmerchant/issues/733). This indicates that the item is out of stock and only available from a third-party seller (often at a markup). 64 | 65 | ## I'm using streetmerchant in the cloud and X isn't working. 66 | 67 | There is _a lot_ of undefined behavior with using streetmerchant in the cloud. Some sites may block IPs from your cloud provider. It is possible that a VPN will help circumvent these problems. 68 | -------------------------------------------------------------------------------- /src/store/model/storm.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const StormComputers: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: 'div.summary.entry-summary > form > button', 9 | text: ['ADD TO CART'], 10 | }, 11 | maxPrice: { 12 | container: '.price', 13 | euroFormat: false, 14 | }, 15 | outOfStock: { 16 | container: 'div.summary.entry-summary > p.stock.out-of-stock', 17 | text: ['Out of stock', 'pre-order', 'preorder'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'colorful', 23 | model: 'igame ultra oc', 24 | series: '3080', 25 | url: 'https://www.stormcomputers.com.au/product/colorful-igame-rtx-3080-ultra/', 26 | }, 27 | { 28 | brand: 'colorful', 29 | model: 'igame advanced oc', 30 | series: '3080', 31 | url: 'https://www.stormcomputers.com.au/product/colorful-igame-geforce-igame-rtx-3080-advanced-10gb-graphics-card/', 32 | }, 33 | { 34 | brand: 'colorful', 35 | model: 'igame vulcan oc', 36 | series: '3080', 37 | url: 'https://www.stormcomputers.com.au/product/igame-geforce-rtx-3080-vulcan-oc-10g-v/', 38 | }, 39 | { 40 | brand: 'asus', 41 | model: 'strix oc', 42 | series: '3090', 43 | url: 'https://www.stormcomputers.com.au/product/asus-geforce-rtx3090-rog-strix-gaming-oc-24gb-gddr6x-rog-strix-rtx3090-o24g-gaming/', 44 | }, 45 | { 46 | brand: 'colorful', 47 | model: 'battle-ax', 48 | series: '3090', 49 | url: 'https://www.stormcomputers.com.au/product/colorful-geforce-rtx-3090-nb/', 50 | }, 51 | { 52 | brand: 'colorful', 53 | model: 'igame advanced oc', 54 | series: '3090', 55 | url: 'https://www.stormcomputers.com.au/product/colorful-igame-geforce-rtx-3090-advanced-oc/', 56 | }, 57 | { 58 | brand: 'amd', 59 | model: '5900x', 60 | series: 'ryzen5900', 61 | url: 'https://www.stormcomputers.com.au/product/amd-ryzen-9-5900x-zen-3-cpu-12c-24t-tdp-105w-boost-up-to-4-8ghz-base-3-7ghz-total-cache-70mb-no-cooler/', 62 | }, 63 | { 64 | brand: 'amd', 65 | model: '5800x', 66 | series: 'ryzen5800', 67 | url: 'https://www.stormcomputers.com.au/product/amd-ryzen-7-5800x-zen-3-cpu-8c-16t-tdp-105w-boost-up-to-4-7ghz-base-3-8ghz-total-cache-36mb-no-cooler/', 68 | }, 69 | { 70 | brand: 'amd', 71 | model: '5600x', 72 | series: 'ryzen5600', 73 | url: 'https://www.stormcomputers.com.au/product/amd-ryzen-5-5600x-zen-3-cpu-6c-12t-tdp-65w-boost-up-to-4-6ghz-base-3-7ghz-total-cache-35mb-wraith-stealth-cooler/', 74 | }, 75 | ], 76 | name: 'storm-computer', 77 | country: 'AU', 78 | }; 79 | -------------------------------------------------------------------------------- /src/store/model/box.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import {getProductLinksBuilder} from './helpers/card'; 3 | 4 | export const Box: Store = { 5 | backoffStatusCodes: [403, 429, 503], 6 | currency: '£', 7 | labels: { 8 | inStock: { 9 | container: '#divBuyButton', 10 | text: ['add to basket'], 11 | }, 12 | maxPrice: { 13 | container: '.p-price', 14 | euroFormat: false, // Note: Box uses non-euroFromat as price seperator 15 | }, 16 | outOfStock: { 17 | container: '#divBuyButton', 18 | text: ['request stock alert', 'coming soon'], 19 | }, 20 | }, 21 | links: [ 22 | { 23 | brand: 'test:brand', 24 | model: 'test:model', 25 | series: 'test:series', 26 | url: 'https://www.box.co.uk/Gigabyte-GeForce-RTX-2080-Super-8GB-Wind_2724554.html', 27 | }, 28 | { 29 | brand: 'sony', 30 | model: 'ps5 console', 31 | series: 'sonyps5c', 32 | url: 'https://www.box.co.uk/CFI-1015A-Sony-Playstation-5-Console_3199689.html', 33 | }, 34 | { 35 | brand: 'sony', 36 | model: 'ps5 digital', 37 | series: 'sonyps5de', 38 | url: 'https://www.box.co.uk/CFI-1015B-Sony-PlayStation-5-Digital-Edition-Conso_3199692.html', 39 | }, 40 | { 41 | brand: 'microsoft', 42 | model: 'xbox series x', 43 | series: 'xboxsx', 44 | url: 'https://www.box.co.uk/RRT-00007-Xbox-Series-X-Console_3201195.html', 45 | }, 46 | { 47 | brand: 'microsoft', 48 | model: 'xbox series s', 49 | series: 'xboxss', 50 | url: 'https://www.box.co.uk/RRS-00007-Xbox-Series-S-Console_3201197.html', 51 | }, 52 | { 53 | brand: 'amd', 54 | model: 'tuf oc', 55 | series: 'rx6900xt', 56 | url: 'https://www.box.co.uk/90YV0GE0-M0NM00-ASUS-Radeon-RX-X6900XT-16GB-OC-Gaming-Gr_3561243.html', 57 | }, 58 | ], 59 | linksBuilder: { 60 | builder: getProductLinksBuilder({ 61 | productsSelector: '.products-right .p-list', 62 | sitePrefix: 'https://www.box.co.uk', 63 | titleSelector: '.p-list-section > h3 > a[href]', 64 | }), 65 | urls: [ 66 | { 67 | series: '3060', 68 | url: 'https://www.box.co.uk/rtx-3060-graphics-cards', 69 | }, 70 | { 71 | series: '3060ti', 72 | url: 'https://www.box.co.uk/rtx-3060-ti-graphics-cards', 73 | }, 74 | { 75 | series: '3070', 76 | url: 'https://www.box.co.uk/rtx-3070-graphics-cards', 77 | }, 78 | { 79 | series: '3080', 80 | url: 'https://www.box.co.uk/rtx-3080-graphics-cards', 81 | }, 82 | { 83 | series: '3090', 84 | url: 'https://www.box.co.uk/rtx-3090-graphics-cards', 85 | }, 86 | ], 87 | }, 88 | name: 'box', 89 | country: 'UK', 90 | waitUntil: 'domcontentloaded', 91 | }; 92 | -------------------------------------------------------------------------------- /src/store/model/igame.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Igamecomputer: Store = { 4 | backoffStatusCodes: [403, 429], 5 | currency: '$', 6 | labels: { 7 | inStock: { 8 | container: 9 | 'div.product-form__controls-group.product-form__controls-group--submit > div > button', 10 | text: ['ADD TO CART'], 11 | }, 12 | maxPrice: { 13 | container: 'div.price__pricing-group > div.price__regular > dd > span', 14 | euroFormat: false, 15 | }, 16 | outOfStock: { 17 | container: 18 | '#product_form_6084255350971 > div.product-form__controls-group.product-form__controls-group--submit > div > button', 19 | text: ['SOLD OUT'], 20 | }, 21 | }, 22 | links: [ 23 | { 24 | brand: 'asus', 25 | model: 'tuf oc', 26 | series: '3080', 27 | url: 'https://www.igamecomputer.com.au/products/a0068?_pos=5&_sid=42c0f4fc6&_ss=r', 28 | }, 29 | { 30 | brand: 'colorful', 31 | model: 'igame ultra oc', 32 | series: '3080', 33 | url: 'https://www.igamecomputer.com.au/products/mc026?_pos=3&_sid=42c0f4fc6&_ss=r', 34 | }, 35 | { 36 | brand: 'colorful', 37 | model: 'igame advanced', 38 | series: '3080', 39 | url: 'https://www.igamecomputer.com.au/products/mc024?_pos=4&_sid=42c0f4fc6&_ss=r', 40 | }, 41 | { 42 | brand: 'colorful', 43 | model: 'igame advanced oc', 44 | series: '3080', 45 | url: 'https://www.igamecomputer.com.au/products/mc025?_pos=2&_sid=42c0f4fc6&_ss=r', 46 | }, 47 | { 48 | brand: 'colorful', 49 | model: 'igame vulcan oc', 50 | series: '3080', 51 | url: 'https://www.igamecomputer.com.au/products/mc023?_pos=1&_sid=42c0f4fc6&_ss=r', 52 | }, 53 | { 54 | brand: 'colorful', 55 | model: 'battle-ax', 56 | series: '3090', 57 | url: 'https://www.igamecomputer.com.au/products/mc022?_pos=1&_sid=b07af5f7e&_ss=r', 58 | }, 59 | { 60 | brand: 'colorful', 61 | model: 'igame advanced oc', 62 | series: '3090', 63 | url: 'https://www.igamecomputer.com.au/products/mc021?_pos=2&_sid=b07af5f7e&_ss=r', 64 | }, 65 | { 66 | brand: 'amd', 67 | model: '5900x', 68 | series: 'ryzen5900', 69 | url: 'https://www.igamecomputer.com.au/products/a0126?_pos=1&_psq=5900x&_ss=e&_v=1.0', 70 | }, 71 | { 72 | brand: 'amd', 73 | model: '5800x', 74 | series: 'ryzen5800', 75 | url: 'https://www.igamecomputer.com.au/products/a0125?_pos=1&_psq=5800x&_ss=e&_v=1.0', 76 | }, 77 | { 78 | brand: 'amd', 79 | model: '5600x', 80 | series: 'ryzen5600', 81 | url: 'https://www.igamecomputer.com.au/products/a0124?_pos=1&_psq=5600x&_ss=e&_v=1.0', 82 | }, 83 | ], 84 | name: 'igame-computer', 85 | country: 'AU', 86 | }; 87 | -------------------------------------------------------------------------------- /src/store/model/awd.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import {getProductLinksBuilder} from './helpers/card'; 3 | 4 | export const Awd: Store = { 5 | currency: '£', 6 | labels: { 7 | inStock: { 8 | container: '.vs-stock .ty-qty-in-stock', 9 | text: ['item(s)'], 10 | }, 11 | maxPrice: { 12 | container: '.ty-price', 13 | euroFormat: false, // Note: Awd uses non-euroFromat as price seperator 14 | }, 15 | outOfStock: { 16 | container: '.vs-stock.ty-float-left', 17 | text: ['Out-of-stock'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'test:brand', 23 | model: 'test:model', 24 | series: 'test:series', 25 | url: 'https://www.awd-it.co.uk/asus-nvidia-geforce-gt-710-silent-low-profile-2gb-gddr5-graphics-card-pci-e.html', 26 | }, 27 | { 28 | brand: 'amd', 29 | model: '5600x', 30 | series: 'ryzen5600', 31 | url: 'https://www.awd-it.co.uk/amd-ryzen-5-5600x-cpu-six-core-3.7ghz-processor-socket-am4-retail.html', 32 | }, 33 | { 34 | brand: 'amd', 35 | model: '5800x', 36 | series: 'ryzen5800', 37 | url: 'https://www.awd-it.co.uk/amd-ryzen-7-5800x-cpu-eight-core-3.8ghz-processor-socket-am4-retail.html', 38 | }, 39 | { 40 | brand: 'amd', 41 | model: '5900x', 42 | series: 'ryzen5900', 43 | url: 'https://www.awd-it.co.uk/amd-ryzen-9-5900x-cpu-twelve-core-3.7ghz-processor-socket-am4-retail.html', 44 | }, 45 | { 46 | brand: 'amd', 47 | model: '5950x', 48 | series: 'ryzen5950', 49 | url: 'https://www.awd-it.co.uk/amd-ryzen-9-5950x-sixteen-core-socket-am4-3.4ghz-processor.html', 50 | }, 51 | ], 52 | linksBuilder: { 53 | builder: getProductLinksBuilder({ 54 | productsSelector: '.grid-list .ty-grid-list__item', 55 | sitePrefix: 'https://www.awd-it.co.uk', 56 | titleSelector: '.product-title', 57 | }), 58 | urls: [ 59 | { 60 | series: 'rx6800', 61 | url: 'https://www.awd-it.co.uk/components/graphics-cards/ati/amd-radeon-6800-6800xt.html', 62 | }, 63 | { 64 | series: '3060ti', 65 | url: 'https://www.awd-it.co.uk/components/graphics-cards/nvidia/nvidia-geforce-rtx-3060ti.html', 66 | }, 67 | { 68 | series: '3070', 69 | url: 'https://www.awd-it.co.uk/components/graphics-cards/nvidia/nvidia-geforce-rtx-3070.html', 70 | }, 71 | { 72 | series: '3080', 73 | url: 'https://www.awd-it.co.uk/components/graphics-cards/nvidia/nvidia-geforce-rtx-3080.html', 74 | }, 75 | { 76 | series: '3090', 77 | url: 'https://www.awd-it.co.uk/components/graphics-cards/nvidia/nvidia-geforce-rtx-3090.html', 78 | }, 79 | ], 80 | }, 81 | name: 'awd', 82 | country: 'UK', 83 | waitUntil: 'domcontentloaded', 84 | }; 85 | -------------------------------------------------------------------------------- /src/store/model/futurex.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Futurex: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.productPriceInner', 8 | text: ['Auf Lager'], 9 | }, 10 | maxPrice: { 11 | container: '.price', 12 | euroFormat: true, 13 | }, 14 | outOfStock: [ 15 | { 16 | container: '.notavail', 17 | text: ['Aktuell nicht verfügbar'], 18 | }, 19 | ], 20 | }, 21 | links: [ 22 | { 23 | brand: 'test:brand', 24 | model: 'test:model', 25 | series: 'test:series', 26 | url: 'https://www.future-x.de/corsair-vengeance-lpx-ddr4-32-gb%3A-2-x-16-gb-dimm-288-pin-3200-mhz-pc4-25600-cl16-135-v-ungepuffert-nicht-ecc-schwarz-p-494897', 27 | }, 28 | { 29 | brand: 'asus', 30 | model: 'tuf oc', 31 | series: '3080', 32 | url: 'https://www.future-x.de/asus-vga-10gb-rtx3080-tuf-gaming-oc-3xdp-2xhdmi-geforce-rtx-3080-grafikkarte-pci-express-10240-mb-displayport-eingang-p-8649614', 33 | }, 34 | { 35 | brand: 'asus', 36 | model: 'strix', 37 | series: '3080', 38 | url: 'https://www.future-x.de/asus-rog-strix-geforce-rtx-3080-10gb-grafikkarte-pci-express-10240-mb-displayport-eingang-p-8649611', 39 | }, 40 | { 41 | brand: 'msi', 42 | model: 'gaming x trio', 43 | series: '3080', 44 | url: 'https://www.future-x.de/msi-geforce-rtx-3080-gaming-x-tr-grafikkarte-10240-mb-p-8649610', 45 | }, 46 | { 47 | brand: 'msi', 48 | model: 'ventus 3x oc', 49 | series: '3080', 50 | url: 'https://www.future-x.de/msi-geforce-rtx-3080ventus-3x10g-oc-grafikkarte-10240-mb-p-8649609', 51 | }, 52 | { 53 | brand: 'zotac', 54 | model: 'amp holo', 55 | series: '3080', 56 | url: 'https://www.future-x.de/zotac-gaming-geforce-rtx-3080-amp-holo-memory-10gb-gddr6x-320-bit-p-8649625', 57 | }, 58 | { 59 | brand: 'zotac', 60 | model: 'trinity', 61 | series: '3080', 62 | url: 'https://www.equippr.de/zotac-geforce-rtx-3080-trinity-10-gb-gddr6x-retail-2060389.html', 63 | }, 64 | { 65 | brand: 'gigabyte', 66 | model: 'aorus elite', 67 | series: 'rx6700xt', 68 | url: 'https://www.future-x.de/gigabyte-radeon-rx6700xt-aorus-elite-12gb-gddr6-2xhdmi-2xd-12288-mb-p-8808313', 69 | }, 70 | { 71 | brand: 'gigabyte', 72 | model: 'gaming oc', 73 | series: 'rx6700xt', 74 | url: 'https://www.future-x.de/gigabyte-radeon-rx-6700-xt-gaming-oc-12gb-gddr6-2xdp-1xhdmi-grafikkarte-12288-mb-p-8787855/', 75 | }, 76 | { 77 | brand: 'sapphire', 78 | model: 'nitro+', 79 | series: 'rx6700xt', 80 | url: 'https://www.future-x.de/sapphire-amd-radeon-rx-6700-xt-gaming-oc-12gb-gddr6-hdmi-12288-mb-p-8808436/', 81 | }, 82 | ], 83 | name: 'futurex', 84 | country: 'DE', 85 | }; 86 | -------------------------------------------------------------------------------- /src/store/model/very.ts: -------------------------------------------------------------------------------- 1 | import {Link, Store} from './store'; 2 | import {logger} from '../../logger'; 3 | import {parseCard} from './helpers/card'; 4 | 5 | export const Very: Store = { 6 | currency: '£', 7 | labels: { 8 | inStock: { 9 | container: '.stockMessaging .indicator', 10 | text: ['available', 'low stock'], 11 | }, 12 | maxPrice: { 13 | container: '.priceNow', 14 | euroFormat: false, // Note: Very uses non-euroFromat as price seperator 15 | }, 16 | outOfStock: { 17 | container: '.stockMessaging .indicator', 18 | text: ['pre-order'], 19 | }, 20 | }, 21 | links: [ 22 | { 23 | brand: 'test:brand', 24 | model: 'test:model', 25 | series: 'test:series', 26 | url: 'https://www.very.co.uk/msi-geforce-gtx-1660-ti-gaming-x-6g-graphics-card/1600350984.prd', 27 | }, 28 | ], 29 | linksBuilder: { 30 | builder: (docElement, series) => { 31 | const productElements = docElement.find('.productList .product'); 32 | const links: Link[] = []; 33 | for (let i = 0; i < productElements.length; i++) { 34 | const productElement = productElements.eq(i); 35 | const titleElement = productElement.find('.productTitle').first(); 36 | const title = titleElement.text()?.replace(/\n/g, ' ').trim(); 37 | 38 | if ( 39 | !title || 40 | ['RTX', series] 41 | .map(x => title.toLowerCase().includes(x.toLowerCase())) 42 | .filter(x => !x).length > 0 43 | ) { 44 | continue; 45 | } 46 | 47 | const url = titleElement.attr()?.href; 48 | 49 | if (!url) { 50 | continue; 51 | } 52 | 53 | const card = parseCard(title); 54 | 55 | if (card) { 56 | links.push({ 57 | brand: card.brand as any, 58 | model: card.model, 59 | series, 60 | url, 61 | }); 62 | } else { 63 | logger.error(`Failed to parse card: ${title}`, {url}); 64 | } 65 | } 66 | 67 | return links; 68 | }, 69 | ttl: 300000, 70 | urls: [ 71 | { 72 | series: '3060ti', 73 | url: 'https://www.very.co.uk/electricals/pc-components/graphics-cards/e/b/118786.end?sort=newin,0&numProducts=100', 74 | }, 75 | { 76 | series: '3070', 77 | url: 'https://www.very.co.uk/electricals/pc-components/graphics-cards/e/b/118786.end?sort=newin,0&numProducts=100', 78 | }, 79 | { 80 | series: '3080', 81 | url: 'https://www.very.co.uk/electricals/pc-components/graphics-cards/e/b/118786.end?sort=newin,0&numProducts=100', 82 | }, 83 | { 84 | series: '3090', 85 | url: 'https://www.very.co.uk/electricals/pc-components/graphics-cards/e/b/118786.end?sort=newin,0&numProducts=100', 86 | }, 87 | ], 88 | }, 89 | name: 'very', 90 | country: 'UK', 91 | }; 92 | -------------------------------------------------------------------------------- /src/store/model/coolmod.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | import {getProductLinksBuilder} from './helpers/card'; 3 | 4 | export const Coolmod: Store = { 5 | currency: '€', 6 | labels: { 7 | inStock: { 8 | container: '#productBuyButtons', 9 | text: ['COMPRAR'], 10 | }, 11 | maxPrice: { 12 | container: '#normalpricenumber', 13 | euroFormat: true, 14 | }, 15 | outOfStock: { 16 | container: '#productBuyButtons', 17 | text: ['NO DISPONIBLE'], 18 | }, 19 | }, 20 | links: [ 21 | { 22 | brand: 'test:brand', 23 | model: 'test:model', 24 | series: 'test:series', 25 | url: 'https://www.coolmod.com/kfa2-geforce-rtx-2060-super-1-click-oc-8gb-gddr6-tarjeta-grafica-precio', 26 | }, 27 | { 28 | brand: 'amd', 29 | model: '5600x', 30 | series: 'ryzen5600', 31 | url: 'https://www.coolmod.com/amd-ryzen-5-5600x-46ghz-socket-am4-boxed-procesador-precio', 32 | }, 33 | { 34 | brand: 'amd', 35 | model: '5800x', 36 | series: 'ryzen5800', 37 | url: 'https://www.coolmod.com/amd-ryzen-7-5800x-47ghz-socket-am4-boxed-procesador-precio', 38 | }, 39 | { 40 | brand: 'amd', 41 | model: '5900x', 42 | series: 'ryzen5900', 43 | url: 'https://www.coolmod.com/amd-ryzen-9-5900x-48ghz-socket-am4-boxed-procesador-precio', 44 | }, 45 | { 46 | brand: 'amd', 47 | model: '5950x', 48 | series: 'ryzen5950', 49 | url: 'https://www.coolmod.com/amd-ryzen-9-5950x-49ghz-socket-am4-boxed-procesador-precio', 50 | }, 51 | ], 52 | name: 'coolmod', 53 | country: 'ES', 54 | linksBuilder: { 55 | builder: getProductLinksBuilder({ 56 | productsSelector: '.productInfo.itemFiltered', 57 | sitePrefix: 'https://www.coolmod.com/', 58 | titleSelector: '.productName a', 59 | }), 60 | ttl: 1, 61 | waitForSelector: '.productInfo.itemFiltered', 62 | urls: [ 63 | { 64 | series: '3060', 65 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9678', 66 | }, 67 | { 68 | series: '3060ti', 69 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9674', 70 | }, 71 | { 72 | series: '3070', 73 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9675', 74 | }, 75 | { 76 | series: '3070ti', 77 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9727', 78 | }, 79 | { 80 | series: '3080', 81 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9676', 82 | }, 83 | { 84 | series: '3080ti', 85 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/9728', 86 | }, 87 | { 88 | series: '3090', 89 | url: 'https://www.coolmod.com/tarjetas-graficas/appliedfilters/8557', 90 | }, 91 | ], 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: streetmerchant 3 | site_url: https://jef.buzz/streetmerchant 4 | site_author: Jef LeCompte 5 | site_description: 🤖 The world's easiest, most powerful stock checker 6 | 7 | # Repository 8 | repo_name: jef/streetmerchant 9 | repo_url: https://github.com/jef/streetmerchant 10 | edit_uri: edit/main/docs/ 11 | 12 | # Copyright 13 | copyright: Copyright © 2016 - 2020 Jef LeCompte 14 | 15 | # Configuration 16 | theme: 17 | custom_dir: docs/overrides 18 | favicon: https://raw.githubusercontent.com/jef/streetmerchant/main/docs/assets/images/streetmerchant-logo.png 19 | features: 20 | - search.suggest 21 | font: 22 | text: Roboto 23 | code: Fira Code 24 | logo: assets/images/streetmerchant-logo.png 25 | language: en 26 | name: material 27 | palette: 28 | - media: "(prefers-color-scheme: light)" 29 | scheme: default 30 | toggle: 31 | icon: material/weather-night 32 | name: Switch to dark mode 33 | - media: "(prefers-color-scheme: dark)" 34 | scheme: slate 35 | toggle: 36 | icon: material/weather-sunny 37 | name: Switch to light mode 38 | 39 | # Plugins 40 | plugins: 41 | - git-revision-date 42 | - macros 43 | - search 44 | 45 | # Customization 46 | extra: 47 | social: 48 | - icon: fontawesome/brands/github 49 | link: https://github.com/jef 50 | - icon: fontawesome/brands/linkedin 51 | link: https://www.linkedin.com/in/jeflecompte 52 | 53 | extra_javascript: 54 | - https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.2.1/tablesort.min.js 55 | - javascripts/tables.js 56 | 57 | # Extensions 58 | markdown_extensions: 59 | - admonition 60 | - attr_list 61 | - footnotes 62 | - meta 63 | - toc: 64 | permalink: true 65 | - pymdownx.caret 66 | - pymdownx.critic 67 | - pymdownx.details 68 | - pymdownx.emoji: 69 | emoji_index: !!python/name:materialx.emoji.twemoji 70 | emoji_generator: !!python/name:materialx.emoji.to_svg 71 | - pymdownx.highlight: 72 | linenums: true 73 | - pymdownx.inlinehilite 74 | - pymdownx.keys 75 | - pymdownx.mark 76 | - pymdownx.smartsymbols 77 | - pymdownx.superfences 78 | - pymdownx.tabbed 79 | - pymdownx.tasklist: 80 | custom_checkbox: true 81 | - pymdownx.tilde 82 | 83 | # Page tree 84 | nav: 85 | - Home: index.md 86 | - Getting started: getting-started.md 87 | - Reference: 88 | - Application: reference/application.md 89 | - Captcha: reference/captcha.md 90 | - Filter: reference/filter.md 91 | - Notification: reference/notification.md 92 | - Proxy: reference/proxy.md 93 | - Terraform: reference/terraform.md 94 | - Help: 95 | - General: help/general.md 96 | - Troubleshoot: help/troubleshoot.md 97 | - FAQ: faq.md 98 | - Changelog: changelog.md 99 | - About: about.md 100 | -------------------------------------------------------------------------------- /src/store/model/otto.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Otto: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: [ 7 | { 8 | container: 9 | 'button.prd_ordering__button.p_btn150--1st.js_product_addToBasket', 10 | text: ['In den Warenkorb'], 11 | }, 12 | ], 13 | maxPrice: { 14 | container: '#normalPriceAmount', 15 | euroFormat: true, 16 | }, 17 | outOfStock: { 18 | container: 'div.p_message.p_message--hint > strong', 19 | text: ['Deinen gewünschten Artikel können wir leider nicht mehr liefern'], 20 | }, 21 | }, 22 | links: [ 23 | { 24 | brand: 'test:brand', 25 | model: 'test:model', 26 | series: 'test:series', 27 | url: 'https://www.otto.de/p/playstation-5-medienfernbedienung-1170617135#variationId=1170617136', 28 | }, 29 | { 30 | brand: 'sony', 31 | labels: { 32 | inStock: { 33 | container: 34 | '.js_shortInfo__variationName.prd_shortInfo__variationName', 35 | text: ['konsole'], 36 | }, 37 | }, 38 | model: 'ps5 console', 39 | series: 'sonyps5c', 40 | url: 'https://www.otto.de/p/playstation-5-1136008456/#variationId=1136008459', 41 | }, 42 | { 43 | brand: 'sony', 44 | labels: { 45 | inStock: { 46 | container: 47 | '.js_shortInfo__variationName.prd_shortInfo__variationName', 48 | text: ['konsole'], 49 | }, 50 | }, 51 | model: 'ps5 console', 52 | series: 'sonyps5c', 53 | url: 'https://www.otto.de/p/playstation-5-1154028000#variationId=1154028001', 54 | }, 55 | { 56 | brand: 'sony', 57 | labels: { 58 | inStock: { 59 | container: 60 | '.js_shortInfo__variationName.prd_shortInfo__variationName', 61 | text: ['konsole'], 62 | }, 63 | }, 64 | model: 'ps5 digital', 65 | series: 'sonyps5de', 66 | url: 'https://www.otto.de/p/playstation-5-digital-edition-1161042793#variationId=1161042794', 67 | }, 68 | { 69 | brand: 'microsoft', 70 | labels: { 71 | inStock: { 72 | container: 73 | '.js_shortInfo__variationName.prd_shortInfo__variationName', 74 | text: ['Xbox Series S'], 75 | }, 76 | }, 77 | model: 'xbox series s', 78 | series: 'xboxss', 79 | url: 'https://www.otto.de/p/xbox-series-s-1229056876/#variationId=1229056877', 80 | }, 81 | { 82 | brand: 'microsoft', 83 | labels: { 84 | inStock: { 85 | container: 86 | '.js_shortInfo__variationName.prd_shortInfo__variationName', 87 | text: ['Xbox Series X'], 88 | }, 89 | }, 90 | model: 'xbox series x', 91 | series: 'xboxsx', 92 | url: 'https://www.otto.de/p/xbox-series-x-1229057353#variationId=1229057354', 93 | }, 94 | ], 95 | name: 'otto', 96 | country: 'DE', 97 | }; 98 | -------------------------------------------------------------------------------- /src/store/model/asus-de.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const AsusDe: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '.buybox--button', 8 | text: ['in den warenkorb'], 9 | }, 10 | }, 11 | links: [ 12 | { 13 | brand: 'test:brand', 14 | model: 'test:model', 15 | series: 'test:series', 16 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2766/asus-rog-strix-rtx2060s-o8g-evo-v2-gaming', 17 | }, 18 | { 19 | brand: 'asus', 20 | model: 'strix', 21 | series: '3080', 22 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2828/asus-rog-strix-rtx3080-10g-gaming', 23 | }, 24 | { 25 | brand: 'asus', 26 | model: 'strix oc', 27 | series: '3080', 28 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2829/asus-rog-strix-rtx3080-o10g-gaming', 29 | }, 30 | { 31 | brand: 'asus', 32 | model: 'tuf', 33 | series: '3080', 34 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2824/asus-tuf-rtx3080-10g-gaming', 35 | }, 36 | { 37 | brand: 'asus', 38 | model: 'tuf oc', 39 | series: '3080', 40 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2825/asus-tuf-rtx3080-o10g-gaming', 41 | }, 42 | { 43 | brand: 'asus', 44 | model: 'strix', 45 | series: '3090', 46 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2826/asus-rog-strix-rtx3090-24g-gaming', 47 | }, 48 | { 49 | brand: 'asus', 50 | model: 'strix oc', 51 | series: '3090', 52 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2827/asus-rog-strix-rtx3090-o24g-gaming', 53 | }, 54 | { 55 | brand: 'asus', 56 | model: 'tuf', 57 | series: '3090', 58 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2822/asus-tuf-rtx3090-24g-gaming', 59 | }, 60 | { 61 | brand: 'asus', 62 | model: 'tuf oc', 63 | series: '3090', 64 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/nvidia-serie/2823/asus-tuf-rtx3090-o24g-gaming', 65 | }, 66 | { 67 | brand: 'asus', 68 | model: 'dual', 69 | series: 'rx6700xt', 70 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/dual-series/3104/asus-dual-rx6700xt-12g', 71 | }, 72 | { 73 | brand: 'asus', 74 | model: 'strix oc', 75 | series: 'rx6700xt', 76 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/rog-serie/3106/asus-rog-strix-rx6700xt-o12g-gaming', 77 | }, 78 | { 79 | brand: 'asus', 80 | model: 'tuf oc', 81 | series: 'rx6700xt', 82 | url: 'https://webshop.asus.com/de/komponenten/grafikkarten/tuf-247-betrieb/3105/asus-tuf-rx6700xt-o12g-gaming', 83 | }, 84 | ], 85 | name: 'asus-de', 86 | country: 'DE', 87 | }; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streetmerchant", 3 | "version": "3.12.0", 4 | "description": "The world's easiest, most powerful stock checker", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "fix": "gts fix", 8 | "lint": "gts lint", 9 | "clean": "gts clean", 10 | "compile": "tsc", 11 | "pretest": "npm run compile", 12 | "posttest": "npm run lint", 13 | "prestart": "npm run compile", 14 | "start": "node build/src/index.js", 15 | "start:dev": "nodemon --config nodemon.json", 16 | "start:production": "node build/src/index.js", 17 | "test": "c8 mocha 'build/test/**/test-*.js' --exclude 'build/test/functional/**/test-*.js'", 18 | "test:notification": "npm run compile && node build/test/functional/test-notification.js", 19 | "test:notification:production": "node build/test/functional/test-notification.js", 20 | "test:captcha": "npm run compile && node build/test/functional/test-captcha.js", 21 | "test:captcha:production": "node build/test/functional/test-captcha.js" 22 | }, 23 | "engines": { 24 | "node": ">=12.0.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/jef/streetmerchant.git" 29 | }, 30 | "keywords": [], 31 | "author": "jef", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/jef/streetmerchant/issues" 35 | }, 36 | "homepage": "https://github.com/jef/streetmerchant#readme", 37 | "dependencies": { 38 | "@doridian/puppeteer-page-proxy": "^1.2.11", 39 | "@jef/pushbullet": "^2.4.3", 40 | "@parse/node-apn": "^5.0.0", 41 | "@slack/web-api": "^6.3.0", 42 | "chalk": "^4.1.2", 43 | "cheerio": "^1.0.0-rc.10", 44 | "discord.js": "^13.0.1", 45 | "dotenv": "^10.0.0", 46 | "messaging-api-telegram": "^1.0.4", 47 | "mqtt": "^4.2.8", 48 | "node-fetch": "^2.6.1", 49 | "node-hue-api": "^4.0.10", 50 | "node-notifier": "^10.0.0", 51 | "node-pagerduty": "^1.3.6", 52 | "nodemailer": "^6.6.3", 53 | "open": "8.2.1", 54 | "play-sound": "^1.1.3", 55 | "puppeteer": "^9.1.1", 56 | "puppeteer-extra-plugin-adblocker": "^2.11.11", 57 | "pushover-notifications": "^1.2.2", 58 | "redis": "^3.1.2", 59 | "top-user-agents": "^1.0.37", 60 | "twilio": "^3.71.1", 61 | "twitter": "^1.7.1", 62 | "winston": "^3.3.3" 63 | }, 64 | "devDependencies": { 65 | "@types/async": "^3.2.7", 66 | "@types/cheerio": "^0.22.30", 67 | "@types/mocha": "^9.0.0", 68 | "@types/node": "^16.4.13", 69 | "@types/node-fetch": "^2.5.12", 70 | "@types/node-notifier": "^8.0.1", 71 | "@types/nodemailer": "^6.4.4", 72 | "@types/redis": "^2.8.31", 73 | "@types/sinon": "^10.0.2", 74 | "@types/twitter": "^1.7.1", 75 | "c8": "^7.8.0", 76 | "gts": "^3.1.0", 77 | "mocha": "^9.0.3", 78 | "nodemon": "^2.0.12", 79 | "sinon": "^11.1.2", 80 | "ts-node": "^10.2.0", 81 | "typescript": "^4.3.5", 82 | "webpack": "^5.50.0" 83 | }, 84 | "volta": { 85 | "node": "16.18.0" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/store/model/galaxus.ts: -------------------------------------------------------------------------------- 1 | import {Store} from './store'; 2 | 3 | export const Galaxus: Store = { 4 | currency: '€', 5 | labels: { 6 | inStock: { 7 | container: '#addToCartButton:enabled', 8 | text: ['In den Warenkorb'], 9 | }, 10 | maxPrice: { 11 | container: '.productDetail .Z1c8', 12 | euroFormat: true, 13 | }, 14 | outOfStock: [ 15 | { 16 | container: '.availabilityText', 17 | text: ['aktuell nicht lieferbar und kein liefertermin vorhanden'], 18 | }, 19 | { 20 | container: '.availabilityText', 21 | text: [ 22 | 'der liefertermin ist beim lieferanten in abklärung und wird aktualisiert.', 23 | ], 24 | }, 25 | ], 26 | }, 27 | links: [ 28 | { 29 | brand: 'test:brand', 30 | model: 'test:model', 31 | series: 'test:series', 32 | url: 'https://www.galaxus.de/de/product/11156643', 33 | }, 34 | { 35 | brand: 'amd', 36 | model: '5600x', 37 | series: 'ryzen5600', 38 | url: 'https://www.galaxus.de/de/product/13987919', 39 | }, 40 | { 41 | brand: 'amd', 42 | model: '5800x', 43 | series: 'ryzen5800', 44 | url: 'https://www.galaxus.de/de/product/13987918', 45 | }, 46 | { 47 | brand: 'amd', 48 | model: '5900x', 49 | series: 'ryzen5900', 50 | url: 'https://www.galaxus.de/de/product/13987917', 51 | }, 52 | { 53 | brand: 'amd', 54 | model: '5950x', 55 | series: 'ryzen5950', 56 | url: 'https://www.galaxus.de/de/product/13987916', 57 | }, 58 | { 59 | brand: 'amd', 60 | model: '5950x', 61 | series: 'ryzen5950', 62 | url: 'https://www.galaxus.de/de/product/13987916', 63 | }, 64 | { 65 | brand: 'asrock', 66 | model: 'challenger', 67 | series: 'rx6700xt', 68 | url: 'https://www.galaxus.de/de/product/15816697', 69 | }, 70 | { 71 | brand: 'asrock', 72 | model: 'phantom gaming oc', 73 | series: 'rx6700xt', 74 | url: 'https://www.galaxus.de/de/product/15948741', 75 | }, 76 | { 77 | brand: 'asus', 78 | model: 'tuf oc', 79 | series: 'rx6700xt', 80 | url: 'https://www.galaxus.de/de/product/15300561', 81 | }, 82 | { 83 | brand: 'gigabyte', 84 | model: 'aorus elite', 85 | series: 'rx6700xt', 86 | url: 'https://www.galaxus.de/de/product/15301182', 87 | }, 88 | { 89 | brand: 'msi', 90 | model: 'mech 2x oc', 91 | series: 'rx6700xt', 92 | url: 'https://www.galaxus.de/de/product/15301319', 93 | }, 94 | { 95 | brand: 'sapphire', 96 | model: 'nitro+ oc', 97 | series: 'rx6700xt', 98 | url: 'https://www.galaxus.de/de/product/15059558', 99 | }, 100 | { 101 | brand: 'sapphire', 102 | model: 'pulse', 103 | series: 'rx6700xt', 104 | url: 'https://www.galaxus.de/de/product/15059559', 105 | }, 106 | ], 107 | name: 'galaxus', 108 | country: 'DE', 109 | }; 110 | -------------------------------------------------------------------------------- /docs/reference/application.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | | Environment variable | Description | 4 | |:---:|---| 5 | | `AUTO_ADD_TO_CART` | Enable auto add to cart on support stores, default: `true` | 6 | | `BROWSER_TRUSTED` | Skip Chromium Sandbox. Useful for containerized environments, default: `false` | 7 | | `HEADLESS` | Puppeteer to run headless or not. Debugging related, default: `true` | 8 | | `INCOGNITO` | Puppeteer to run incognito or not. Debugging related, default: `false` | 9 | | `IN_STOCK_WAIT_TIME` | Time to wait between requests to the same link if it has that card in stock. In seconds, default: `0` | 10 | | `LOG_LEVEL` | [Logging levels](https://github.com/winstonjs/winston#logging-levels). Debugging related, default: `info` | 11 | | `LOW_BANDWIDTH` | Blocks images/fonts to reduce traffic. Disables ad blocker, default: `false` | 12 | | `NVIDIA_ADD_TO_CART_ATTEMPTS` | Maximum number of attempts add an item to card in the Nvidia storefront, default: `10` | 13 | | `NVIDIA_SESSION_TTL` | Maximum session length on the Nvidia storefront in ms, default: `60000` | 14 | | `OPEN_BROWSER` | Toggle for whether or not the browser should open when item is found, default: `true` | 15 | | `PAGE_BACKOFF_MIN` | Minimum backoff time between retrying requests for the same store when a forbidden response is received, default: `10000` | 16 | | `PAGE_BACKOFF_MAX` | Maximum backoff time between retrying requests for the same store when a forbidden response is received, default: `3600000` | 17 | | `PAGE_SLEEP_MIN` | Minimum sleep time between queries of the same product page. In milliseconds, default: `5000` | 18 | | `PAGE_SLEEP_MAX` | Maximum sleep time between queries of the same product page. In milliseconds, default: `10000` | 19 | | `PAGE_TIMEOUT` | Navigation Timeout in milliseconds. `0` for infinite, default: `30000` | 20 | | `PROXY_PROTOCOL` | Protocol of proxy server, such as `socks5`, default: `http` | 21 | | `PROXY_ADDRESS` | IP Address or fqdn of proxy server | 22 | | `PROXY_PORT` | TCP Port number on which the proxy is listening for connections, default: `80` | 23 | | `RESTART_TIME` | Restarts chrome after defined milliseconds. `0` for never, default: `0` | 24 | | `SCREENSHOT` | Capture screenshot of page if a card is found, default: `true` | 25 | | `SCREENSHOT_DIR` | The directory for saving the screenshots, default: `screenshots` | 26 | | `USER_AGENT` | Custom user agent used for requests | 27 | | `WEB_PORT` | Starts a webserver to be able to control the bot while it is running. Setting this value starts this service. | 28 | 29 | ???+ info 30 | There is more information on proxy settings in the [Proxy documentation](proxy.md). 31 | 32 | ???+ tip 33 | - You can also have a list of proxies that are rotated while searching stores. Proxies can be read from a file named `STORENAME.proxies` in the format of `socks5://username:password@ip`; one per line. 34 | - Data usage is [known to be high](https://github.com/jef/streetmerchant/issues?q=is%3Aissue+sort%3Aupdated-desc+bandwidth). This is expected as the program scrapes many websites in parallel 24/7. To help reduce this, use `LOW_BANDWIDTH="true"`. We are looking into other solutions as well, but is low priority. 35 | --------------------------------------------------------------------------------