import os import secrets import time from collections import defaultdict, deque from threading import Lock, Thread from flask import Flask, jsonify, render_template, request from scraper import ScrapeError, get_rating_distribution, validate_maps_url app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 4096 _scrape_lock = Lock() # jeden Chromium naraz, żeby nie zamęczyć RPi _rate_lock = Lock() _requests = defaultdict(deque) _jobs = {} _jobs_lock = Lock() def _rate_limited(ip, limit=6, window=60): now = time.monotonic() with _rate_lock: attempts = _requests[ip] while attempts and attempts[0] < now - window: attempts.popleft() if len(attempts) >= limit: return True attempts.append(now) return False @app.after_request def headers(response): response.headers["Content-Security-Policy"] = ( "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; " "object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" ) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Referrer-Policy"] = "no-referrer" response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" return response @app.errorhandler(413) def too_large(_error): return jsonify(error="Żądanie jest zbyt duże."), 413 @app.get("/") def index(): return render_template("index.html") @app.post("/api/reviews") def reviews(): if _rate_limited(request.remote_addr or "unknown"): return jsonify(error="Za dużo prób. Poczekaj minutę."), 429 data = request.get_json(silent=True) if not isinstance(data, dict) or not isinstance(data.get("url"), str): return jsonify(error="Podaj link do miejsca w Google Maps."), 400 try: url = validate_maps_url(data["url"]) except ValueError as exc: return jsonify(error=str(exc)), 400 if not _scrape_lock.acquire(blocking=False): return jsonify(error="Trwa już inne sprawdzanie. Spróbuj za chwilę."), 429 job_id = secrets.token_urlsafe(24) with _jobs_lock: cutoff = time.monotonic() - 600 for old_id in [key for key, job in _jobs.items() if job["created"] < cutoff]: del _jobs[old_id] _jobs[job_id] = { "status": "running", "progress": 0, "message": "Rozpoczynam sprawdzanie", "created": time.monotonic(), } Thread(target=_run_job, args=(job_id, url), daemon=True).start() return jsonify(job_id=job_id), 202 def _run_job(job_id, url): def update(percent, message): with _jobs_lock: _jobs[job_id].update(progress=percent, message=message) try: result = get_rating_distribution(url, progress=update) with _jobs_lock: _jobs[job_id].update(status="complete", progress=100, message="Gotowe", result=result) except ScrapeError as exc: with _jobs_lock: _jobs[job_id].update(status="error", message=str(exc)) except Exception: app.logger.exception("Unexpected scraper failure") with _jobs_lock: _jobs[job_id].update( status="error", message="Google Maps nie odpowiedziało poprawnie. Spróbuj później." ) finally: _scrape_lock.release() @app.get("/api/reviews/") def review_status(job_id): with _jobs_lock: job = _jobs.get(job_id) if not job: return jsonify(error="Nie znaleziono tego sprawdzenia."), 404 response = {key: value for key, value in job.items() if key != "created"} return jsonify(response) if __name__ == "__main__": app.run(host=os.environ.get("HOST", "127.0.0.1"), port=int(os.environ.get("PORT", 8080)), debug=False)