From dcd2e0d3c46eb014a8a81fd8372e2d609d4a2b1c Mon Sep 17 00:00:00 2001 From: WOBBLEFANG THE THIRD Date: Fri, 23 Jan 2026 13:27:52 +0100 Subject: [PATCH] feat: update stock monitoring to include Discord user mentions and validation --- Dockerfile | 2 +- main.py | 95 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index ab9971c..b06f35d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,6 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY monitor.py . +COPY main.py . CMD ["python", "main.py"] diff --git a/main.py b/main.py index 51d2ee9..409777d 100644 --- a/main.py +++ b/main.py @@ -9,9 +9,12 @@ import requests # Configuration # ========================= -PRODUCT_URL = f"{os.environ["PRODUCT_URL"]}.js" -TARGET_VARIANT_ID = os.environ["TARGET_VARIANT_ID"] -DISCORD_WEBHOOK_URL = os.environ["DISCORD_WEBHOOK_URL"] +DISCORD_USER_ID_TO_PING = "736200712169455656" +DISCORD_MENTION = f"<@{DISCORD_USER_ID_TO_PING}>" + +PRODUCT_URL = f"{os.environ.get('PRODUCT_URL', '').rstrip('/')}.js" +TARGET_VARIANT_ID_RAW = os.environ.get("TARGET_VARIANT_ID", "") +DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL", "") CHECK_INTERVAL_SECONDS = 30 POST_DETECTION_COOLDOWN_SECONDS = 600 @@ -36,54 +39,101 @@ console_handler.setFormatter(log_format) logger.addHandler(console_handler) +def _require_env(name: str, value: str) -> str: + """Return the env var value; raises if missing or empty.""" + value = (value or "").strip() + if not value: + raise RuntimeError(f"Missing required environment variable: {name}") + return value + + +def _normalize_variant_id(raw: str) -> str: + """Return a string form of the variant id; raises if invalid.""" + raw = (raw or "").strip() + if not raw: + raise RuntimeError("TARGET_VARIANT_ID is empty") + try: + return str(int(raw)) + except ValueError as exc: + raise RuntimeError("TARGET_VARIANT_ID must be an integer-like value") from exc + + +def validate_config() -> tuple[str, str, str]: + """Validate required config and return normalized (product_url, variant_id, webhook_url).""" + product_url = _require_env("PRODUCT_URL", PRODUCT_URL) + variant_id = _normalize_variant_id(TARGET_VARIANT_ID_RAW) + webhook_url = _require_env("DISCORD_WEBHOOK_URL", DISCORD_WEBHOOK_URL) + + if not webhook_url.startswith("https://"): + raise RuntimeError("DISCORD_WEBHOOK_URL must start with https://") + + return product_url, variant_id, webhook_url + + # ========================= # Discord Notification # ========================= def send_discord_notification(product_name, variant_name, checkout_link): - """ - Sends a stock alert to Discord using a webhook. - """ - + """Sends a stock alert to Discord using a webhook.""" logger.critical("Stock detected! Sending Discord notification.") - payload = {"content": ("🚨 **RESTOCK ALERT** 🚨\n\n" + payload = {"content": (f"{DISCORD_MENTION}\n" + "🚨 **RESTOCK ALERT** 🚨\n\n" f"**{product_name}**\n" f"Variant: {variant_name}\n\n" f"👉 [ONE-CLICK CHECKOUT]({checkout_link})")} try: - response = requests.post(DISCORD_WEBHOOK_URL, json=payload) + response = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10) if response.status_code == 204: logger.info("Discord notification sent successfully.") else: logger.error("Discord notification failed. " - f"HTTP status code: {response.status_code}") + f"HTTP status code: {response.status_code}; response: {response.text[:500]}") except Exception as exception: logger.error("Failed to send Discord notification due to an exception.", exc_info=exception) +def send_startup_test_ping(): + """Sends a one-time hello-world message to validate webhook + mentions.""" + logger.info("Sending startup Discord test ping...") + + payload = {"content": (f"{DISCORD_MENTION} Hello world! " + "✅ Webhook connection is working and user mention should ping.")} + + try: + response = requests.post(DISCORD_WEBHOOK_URL, json=payload, timeout=10) + + if response.status_code == 204: + logger.info("Startup Discord test ping sent successfully.") + else: + logger.error("Startup Discord test ping failed. " + f"HTTP status code: {response.status_code}; response: {response.text[:500]}") + + except Exception as exception: + logger.error("Failed to send startup test ping due to an exception.", exc_info=exception) + + # ========================= # Stock Monitoring Logic # ========================= def check_stock_continuously(): - """ - Continuously checks the product JSON endpoint to see if the target - variant is available for purchase. - """ + """Continuously checks the product JSON endpoint to see if the target variant is available for purchase.""" + product_url, target_variant_id, _ = validate_config() - logger.info(f"Stock monitor started. Watching variant ID: {TARGET_VARIANT_ID}") + logger.info(f"Stock monitor started. Watching variant ID: {target_variant_id}") request_headers = {"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/120.0.0.0 Safari/537.36"), "Accept": "application/json"} + "Chrome/120.0.0.0 Safari/537.36"), "Accept": "application/json", } while True: try: - response = requests.get(PRODUCT_URL, headers=request_headers, timeout=10) + response = requests.get(product_url, headers=request_headers, timeout=10) if response.status_code != 200: logger.error(f"HTTP request failed. Status code: {response.status_code}") @@ -94,10 +144,11 @@ def check_stock_continuously(): product_name = product_data.get("title", "FormD T1") variants = product_data.get("variants", []) - target_variant = next((variant for variant in variants if variant.get("id") == TARGET_VARIANT_ID), None) + target_variant = next((variant for variant in variants if str(variant.get("id")) == target_variant_id), + None, ) if not target_variant: - logger.error(f"Variant ID {TARGET_VARIANT_ID} was not found in the product data.") + logger.error(f"Variant ID {target_variant_id} was not found in the product data.") time.sleep(CHECK_INTERVAL_SECONDS) continue @@ -107,7 +158,7 @@ def check_stock_continuously(): if is_in_stock: logger.warning(f"Stock available! Variant detected: {variant_name}") - checkout_link = (f"https://formdt1.com/cart/{TARGET_VARIANT_ID}:1") + checkout_link = f"https://formdt1.com/cart/{target_variant_id}:1" send_discord_notification(product_name, variant_name, checkout_link) @@ -123,7 +174,7 @@ def check_stock_continuously(): except requests.exceptions.Timeout: logger.warning("Request timed out. The server may be slow or blocking requests.") - except Exception as exception: + except Exception: logger.exception("An unexpected error occurred during the stock check.") time.sleep(CHECK_INTERVAL_SECONDS) @@ -134,4 +185,6 @@ def check_stock_continuously(): # ========================= if __name__ == "__main__": + validate_config() + send_startup_test_ping() check_stock_continuously()