Spaces:
Running
Running
Download routes/help.py from Success-dev/venthrive-api: direct link, hf CLI and curl.
- Browser
- Download file 2.87 kB
-
https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/routes/help.py
- Command line
-
hf download hf://spaces/Success-dev/venthrive-api/routes/help.py
-
curl -L -o help.py https://huggingface.co/spaces/Success-dev/venthrive-api/resolve/main/routes/help.py
2.87 kB
| """ | |
| Help-centre feedback — the "Was this page helpful?" votes. | |
| Public (no sign-in), so it stores no user id, name or email: only the page, the | |
| vote, an optional reason and comment, and an anonymous per-browser-session id. | |
| Comments are de-identified after 30 days by scripts/deidentify_help_feedback.py. | |
| Writes go through here (never straight from the browser); firestore.rules deny | |
| all client access to help_feedback. | |
| """ | |
| import time | |
| import threading | |
| from collections import defaultdict | |
| from typing import Literal, Optional | |
| from fastapi import APIRouter, HTTPException | |
| from firebase_admin import firestore | |
| from pydantic import BaseModel, Field | |
| router = APIRouter() | |
| REASONS = {"not_found", "steps_failed", "out_of_date", "unclear", "other"} | |
| RETENTION_SECONDS = 30 * 24 * 3600 | |
| # In-memory, per process (same trade-off as utils/rate_limiter.py). Keyed by the | |
| # anonymous session id, plus a global cap so one client can't flood the collection. | |
| PER_SESSION_LIMIT = (30, 3600) # 30 votes per hour | |
| GLOBAL_LIMIT = (600, 3600) # 600 votes per hour, all sessions | |
| _lock = threading.Lock() | |
| _hits: dict[str, list[float]] = defaultdict(list) | |
| def _allow(key: str, limit: tuple[int, int]) -> bool: | |
| count, period = limit | |
| now = time.time() | |
| with _lock: | |
| recent = [t for t in _hits[key] if now - t < period] | |
| if len(recent) >= count: | |
| _hits[key] = recent | |
| return False | |
| recent.append(now) | |
| _hits[key] = recent | |
| return True | |
| class FeedbackIn(BaseModel): | |
| pageId: str = Field(min_length=1, max_length=200, pattern=r"^[a-z0-9/_-]+$") | |
| pageVersion: str = Field(min_length=1, max_length=40) | |
| vote: Literal["yes", "no"] | |
| reason: Optional[str] = Field(default=None, max_length=30) | |
| comment: Optional[str] = Field(default=None, max_length=1000) | |
| sessionId: str = Field(min_length=1, max_length=64) | |
| device: Literal["mobile", "desktop"] = "desktop" | |
| def submit_feedback(body: FeedbackIn): | |
| if not _allow(f"s:{body.sessionId}", PER_SESSION_LIMIT) or not _allow("global", GLOBAL_LIMIT): | |
| raise HTTPException(status_code=429, detail="Too many requests. Try again later.") | |
| reason = body.reason if body.reason in REASONS else None | |
| now = time.time() | |
| doc = { | |
| "pageId": body.pageId, | |
| "pageVersion": body.pageVersion, | |
| "vote": body.vote, | |
| "reason": reason, | |
| "comment": (body.comment or "").strip() or None, | |
| "sessionId": body.sessionId, | |
| "device": body.device, | |
| "at": firestore.SERVER_TIMESTAMP, | |
| "deidentifyAfter": now + RETENTION_SECONDS, | |
| "deidentified": False, | |
| } | |
| try: | |
| firestore.client().collection("help_feedback").add(doc) | |
| except Exception: | |
| raise HTTPException(status_code=500, detail="Could not save feedback.") | |
| return {"success": True} | |