Improve feedback API resilience
This commit is contained in:
@@ -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,15 +76,25 @@ 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()
|
||||||
|
with _feedback_lock:
|
||||||
|
_prune_feedback_cache(now)
|
||||||
last = _last_feedback_by_ip.get(ip, 0)
|
last = _last_feedback_by_ip.get(ip, 0)
|
||||||
|
|
||||||
if now - last < COOLDOWN_SECONDS:
|
if now - last < COOLDOWN_SECONDS:
|
||||||
@@ -98,8 +132,10 @@ 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
|
||||||
|
|
||||||
|
with _feedback_lock:
|
||||||
_last_feedback_by_ip[ip] = now
|
_last_feedback_by_ip[ip] = now
|
||||||
return jsonify({"ok": True}), 200
|
return jsonify({"ok": True}), 200
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user