Improve feedback API resilience

This commit is contained in:
2026-01-20 10:41:19 +01:00
parent 4edb25106c
commit 02c91161be
+39 -3
View File
@@ -1,8 +1,13 @@
import logging
import os import os
import threading
import time import time
from urllib.error import URLError, HTTPError from urllib.error import URLError, HTTPError
import requests import requests
from flask import Flask, render_template, jsonify, request, Blueprint 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 from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__) app = Flask(__name__)
@@ -13,6 +18,10 @@ DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
COOLDOWN_SECONDS = 60 * 60 COOLDOWN_SECONDS = 60 * 60
_last_feedback_by_ip = {} _last_feedback_by_ip = {}
_feedback_lock = threading.Lock()
logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)
PROHIBITED_WORDS = { PROHIBITED_WORDS = {
'fuck', 'shit', 'asshole', 'bitch', 'bastard', 'damn', 'crap', 'dick', 'piss', 'cunt', '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) 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: def _post_to_discord(payload: dict) -> None:
if not DISCORD_WEBHOOK_URL: if not DISCORD_WEBHOOK_URL:
raise RuntimeError("DISCORD_WEBHOOK_URL is not set on the server.") 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", "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): if resp.status_code not in (200, 204):
raise RuntimeError(f"Discord returned {resp.status_code}: {resp.text[:500]}") 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") @api.post("/feedback")
def api_feedback(): def api_feedback():
ip = _get_client_ip() ip = _get_client_ip()
now = time.time() 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: if now - last < COOLDOWN_SECONDS:
retry_after = int(COOLDOWN_SECONDS - (now - last)) retry_after = int(COOLDOWN_SECONDS - (now - last))
@@ -98,9 +132,11 @@ def api_feedback():
try: try:
_post_to_discord({"username": name, "content": content}) _post_to_discord({"username": name, "content": content})
except (HTTPError, URLError, RuntimeError) as e: 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 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 return jsonify({"ok": True}), 200