From 02c91161bedc74ab88d19a0a8ff73642dbebb979 Mon Sep 17 00:00:00 2001 From: Fretux Date: Tue, 20 Jan 2026 10:41:19 +0100 Subject: [PATCH] Improve feedback API resilience --- app.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 19cf399..6cf7423 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,13 @@ +import logging import os +import threading import time from urllib.error import URLError, HTTPError + import requests from flask import Flask, render_template, jsonify, request, Blueprint +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from werkzeug.middleware.proxy_fix import ProxyFix app = Flask(__name__) @@ -13,6 +18,10 @@ DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") COOLDOWN_SECONDS = 60 * 60 _last_feedback_by_ip = {} +_feedback_lock = threading.Lock() + +logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO")) +logger = logging.getLogger(__name__) PROHIBITED_WORDS = { 'fuck', 'shit', 'asshole', 'bitch', 'bastard', 'damn', 'crap', 'dick', 'piss', 'cunt', @@ -39,6 +48,21 @@ def _contains_prohibited_word(text: str) -> bool: return any(w in lower for w in PROHIBITED_WORDS) +def _get_discord_session() -> requests.Session: + session = requests.Session() + retry = Retry( + total=3, + backoff_factor=0.5, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=("POST",), + raise_on_status=False, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount("https://", adapter) + session.mount("http://", adapter) + return session + + def _post_to_discord(payload: dict) -> None: if not DISCORD_WEBHOOK_URL: raise RuntimeError("DISCORD_WEBHOOK_URL is not set on the server.") @@ -52,16 +76,26 @@ def _post_to_discord(payload: dict) -> None: "Accept": "application/json", } - resp = requests.post(url, json=payload, headers=headers, timeout=10) + session = _get_discord_session() + resp = session.post(url, json=payload, headers=headers, timeout=10) if resp.status_code not in (200, 204): raise RuntimeError(f"Discord returned {resp.status_code}: {resp.text[:500]}") +def _prune_feedback_cache(now: float) -> None: + stale_cutoff = now - COOLDOWN_SECONDS + stale_ips = [ip for ip, ts in _last_feedback_by_ip.items() if ts < stale_cutoff] + for ip in stale_ips: + _last_feedback_by_ip.pop(ip, None) + + @api.post("/feedback") def api_feedback(): ip = _get_client_ip() now = time.time() - last = _last_feedback_by_ip.get(ip, 0) + with _feedback_lock: + _prune_feedback_cache(now) + last = _last_feedback_by_ip.get(ip, 0) if now - last < COOLDOWN_SECONDS: retry_after = int(COOLDOWN_SECONDS - (now - last)) @@ -98,9 +132,11 @@ def api_feedback(): try: _post_to_discord({"username": name, "content": content}) except (HTTPError, URLError, RuntimeError) as e: + logger.exception("Failed to forward feedback to Discord") return jsonify({"ok": False, "error": f"Failed to forward feedback: {str(e)}"}), 502 - _last_feedback_by_ip[ip] = now + with _feedback_lock: + _last_feedback_by_ip[ip] = now return jsonify({"ok": True}), 200