feat: update stock monitoring to include Discord user mentions and validation

This commit is contained in:
2026-01-23 13:27:52 +01:00
parent bc126e422e
commit dcd2e0d3c4
2 changed files with 75 additions and 22 deletions

95
main.py
View File

@@ -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()