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__) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 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', 'motherfucker', 'douche', 'prick', 'whore', 'slut', 'fag', 'nigger', 'spic', 'retard', 'twat', 'bollocks', 'wanker', 'bugger', 'arse', 'shithead', 'dumbass', 'jackass', 'dipshit', 'dildo', 'knobhead', 'ballsack', 'buttfucker', 'cum', 'jizz', 'cocksucker', 'fucker', 'skank', 'pussy', 'tits', 'clit', 'schlong', 'dickhead', 'goddamn', 'hell', 'bitchass', 'fuckface', 'shitfaced', 'asshat', 'bint', 'chink', 'gook', 'jap', 'negro', 'dyke', 'queer', 'tranny', 'tosser', 'spaz', 'mong', 'coon', 'beaner', 'cracker', 'wetback', 'homo', 'fudgepacker', 'tard', 'cocks', 'numbnuts', 'knob', 'muppet', 'git', 'shag', 'minge', 'nonce', 'plonker', 'pillock' } api = Blueprint("api", __name__, url_prefix="/api") def _get_client_ip() -> str: xff = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() return xff or (request.remote_addr or "unknown") def _contains_prohibited_word(text: str) -> bool: lower = text.lower() 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.") url = DISCORD_WEBHOOK_URL.strip() if "?" not in url: url += "?wait=true" headers = { "User-Agent": "TRLStatsFeedback/1.0 (contact: fretux@fretux.ch)", "Content-Type": "application/json", "Accept": "application/json", } 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() 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)) return jsonify({ "ok": False, "error": f"Please wait {retry_after} seconds before submitting again." }), 429 data = request.get_json(silent=True) or {} name = (data.get("name") or "Anonymous").strip() rating = data.get("rating") comment = (data.get("comment") or "").strip() if len(name) > 32: name = name[:32] try: rating_int = int(rating) except (TypeError, ValueError): return jsonify({"ok": False, "error": "Rating must be a number."}), 400 if rating_int < 1 or rating_int > 10: return jsonify({"ok": False, "error": "Rating must be between 1 and 10."}), 400 if len(comment) < 5 or len(comment) > 500: return jsonify({"ok": False, "error": "Comment must be between 5 and 500 characters."}), 400 if _contains_prohibited_word(comment): return jsonify({"ok": False, "error": "Your comment contains inappropriate content."}), 400 content = f"**Rating:** {rating_int}\n**Comment:**\n{comment}" 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 with _feedback_lock: _last_feedback_by_ip[ip] = now return jsonify({"ok": True}), 200 app.register_blueprint(api) @app.route("/") def index(): return render_template("index.html") @app.route("/calculator") def calculator(): return render_template("calculator.html") @app.route("/about_us") def about_us(): return render_template("about_us.html") @app.route("/suggestion") def suggestion(): return render_template("suggestion.html") if __name__ == '__main__': app.run(debug=True)