"""Owned, consented feedback with durable duplicate and storage limits.""" from __future__ import annotations import hashlib import json import re from uuid import UUID from shared.database import transaction LOCK = 5787785515774603126 RETENTION_DAYS = 7 MAX_ROWS = 500 MAX_BYTES = 49152 class FeedbackError(RuntimeError): def __init__(self, message: str, *, status: int = 400): self.status = status super().__init__(message) def cleanup_feedback(conn, batch: int = 100) -> int: return conn.execute( "DELETE FROM precedent_private.feedback WHERE id IN " "(SELECT id FROM precedent_private.feedback " "WHERE created_at <= clock_timestamp()-interval '7 days' " "ORDER BY created_at LIMIT %s)", (batch,), ).rowcount def _receipt(row, *, duplicate: bool = False) -> dict: return {"id": str(row["id"]), "created_at": row["created_at"].isoformat(), "status": "withdrawn" if row["withdrawn_at"] else "received", "already_received": duplicate, "retention_days": RETENTION_DAYS} class Feedback: def __init__(self, dsn: str): self.dsn = dsn @staticmethod def _owner(owner: str) -> str: if not re.fullmatch(r"[a-f0-9]{32}", owner): raise FeedbackError("Use the authenticated website proxy.", status=401) return owner def submit(self, owner: str, ident: UUID, payload: dict) -> dict: from psycopg.types.json import Jsonb owner = self._owner(owner) if payload.get("consent") is not True: raise FeedbackError("Confirm sharing this report before submitting it.") if payload.get("context") is not None and payload.get("share_context") is not True: raise FeedbackError("Including a question and answer needs separate consent.") encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode() if len(encoded) > MAX_BYTES: raise FeedbackError("Please shorten the report or included answer.", status=413) fingerprint = hashlib.sha256(encoded).hexdigest() with transaction(self.dsn, lock=LOCK) as conn: cleanup_feedback(conn) existing = conn.execute( "SELECT * FROM precedent_private.feedback WHERE id=%s", (ident,), ).fetchone() if existing: if existing["owner_key"] != owner or existing["fingerprint"] != fingerprint: raise FeedbackError("This submission ID belongs to a different report.", status=409) return _receipt(existing, duplicate=True) counts = conn.execute( "SELECT count(*) AS total, " "count(*) FILTER (WHERE created_at>clock_timestamp()-interval '1 day') AS daily, " "count(*) FILTER (WHERE owner_key=%s AND " "created_at>clock_timestamp()-interval '1 day') AS personal " "FROM precedent_private.feedback", (owner,), ).fetchone() if counts["total"] >= MAX_ROWS or counts["daily"] >= 100 or counts["personal"] >= 10: raise FeedbackError("The feedback allowance is full. Please try again tomorrow.", status=429) row = conn.execute( "INSERT INTO precedent_private.feedback(id,owner_key,fingerprint,category,payload) " "VALUES (%s,%s,%s,%s,%s) RETURNING *", (ident, owner, fingerprint, payload["category"], Jsonb(payload)), ).fetchone() return _receipt(row) def list(self, owner: str) -> list[dict]: owner = self._owner(owner) with transaction(self.dsn) as conn: rows = conn.execute( "SELECT * FROM precedent_private.feedback WHERE owner_key=%s " "AND withdrawn_at IS NULL AND created_at>clock_timestamp()-interval '7 days' " "ORDER BY created_at DESC,id LIMIT 20", (owner,), ).fetchall() return [{**_receipt(row), "report": row["payload"]} for row in rows] def withdraw(self, owner: str, ident: UUID) -> None: owner = self._owner(owner) with transaction(self.dsn, lock=LOCK) as conn: conn.execute( "UPDATE precedent_private.feedback SET payload='{}'::jsonb, " "withdrawn_at=COALESCE(withdrawn_at,clock_timestamp()) " "WHERE id=%s AND owner_key=%s", (ident, owner), )