nomadweb/backend/app/services/platform_store.py
eric f215b684f6 Wire connect-ring notifications and durable meetup RSVP.
Require login for meetup RSVP with cancel, notify organizers/authors/posters on RSVP/reply/gig/match, optional ntfy + Listmonk hooks, and honest newsletter messaging.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 19:33:07 -05:00

205 lines
6.1 KiB
Python

"""Platform persistence — PocketBase backed."""
from __future__ import annotations
import json
import secrets
from datetime import datetime, timezone
from pathlib import Path
from app.services.pb_repo import q, safe_create, safe_first, safe_list, safe_update, use_pb
from app.config import settings
_JSON_PATH = Path(__file__).resolve().parents[1] / "data" / "platform_store.json"
_j: dict = {}
def _now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
def _jload() -> dict:
global _j
if _j:
return _j
if _JSON_PATH.exists():
try:
_j = json.loads(_JSON_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
_j = {}
else:
_j = {}
return _j
def _jsave() -> None:
_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
_JSON_PATH.write_text(json.dumps(_j, ensure_ascii=False), encoding="utf-8")
def add_service_lead(service_id: str, user_id: str | None, name: str, email: str, message: str) -> dict:
item = {
"serviceSlug": service_id,
"userId": user_id or "",
"email": email,
"note": f"{name}: {message}"[:1000],
"status": "open",
}
if use_pb():
row = safe_create("service_leads", item)
return {"id": row["id"] if row else secrets.token_hex(6), **item}
_jload()
out = {"id": secrets.token_hex(6), "service_id": service_id, "name": name, "email": email, "message": message, "created_at": _now()}
_j.setdefault("leads", []).append(out)
_jsave()
return out
def add_content_submission(user_id: str | None, name: str, title: str, content_type: str, url: str, notes: str) -> dict:
if use_pb():
row = safe_create(
"content_submissions",
{
"title": title,
"type": content_type,
"mediaUrl": url,
"submittedBy": user_id or "",
"authorName": name,
"description": notes[:2000],
"reviewStatus": "pending",
},
)
return {"id": row["id"] if row else secrets.token_hex(6), "title": title, "status": "pending"}
_jload()
out = {"id": secrets.token_hex(6), "title": title, "content_type": content_type, "url": url, "status": "pending", "created_at": _now()}
_j.setdefault("submissions", []).append(out)
_jsave()
return out
def add_volunteer(city_slug: str, user_id: str | None, name: str, email: str, message: str) -> dict:
if use_pb():
row = safe_create(
"volunteer_applications",
{
"citySlug": city_slug,
"userId": user_id or "",
"applicantName": name,
"email": email,
"motivation": message[:2000],
"status": "pending",
},
)
return {"id": row["id"] if row else secrets.token_hex(6), "city_slug": city_slug}
_jload()
out = {"id": secrets.token_hex(6), "city_slug": city_slug, "name": name, "email": email, "created_at": _now()}
_j.setdefault("volunteers", []).append(out)
_jsave()
return out
def add_report(reporter_id: str | None, target_type: str, target_id: str, reason: str) -> dict:
if use_pb():
row = safe_create(
"reports",
{
"reporterId": reporter_id or "",
"targetType": target_type,
"targetId": target_id,
"reason": reason[:2000],
"status": "open",
},
)
return {"id": row["id"] if row else secrets.token_hex(6), "status": "open"}
_jload()
out = {"id": secrets.token_hex(6), "target_type": target_type, "target_id": target_id, "status": "open", "created_at": _now()}
_j.setdefault("reports", []).append(out)
_jsave()
return out
def subscribe_newsletter(email: str, source: str = "site") -> dict:
"""Save subscription locally; sync to Listmonk when configured."""
email = email.strip().lower()
synced = False
if settings.listmonk_url and settings.listmonk_user:
try:
import httpx
base = settings.listmonk_url.rstrip("/")
auth = (settings.listmonk_user, settings.listmonk_password)
payload = {
"email": email,
"name": email.split("@")[0],
"status": "enabled",
"lists": [settings.listmonk_list_id],
}
with httpx.Client(timeout=8.0) as client:
r = client.post(f"{base}/api/subscribers", json=payload, auth=auth)
if r.status_code in (200, 409):
synced = True
elif r.status_code == 400 and "exists" in (r.text or "").lower():
synced = True
except Exception:
synced = False
if use_pb():
dup = safe_first("subscriptions", filter=f"email={q(email)}")
if dup:
return {
"ok": True,
"message": "已订阅" if synced else "已登记订阅(邮件系统稍后开通)",
"synced": synced,
}
safe_create("subscriptions", {"email": email})
return {
"ok": True,
"message": "订阅成功" if synced else "已登记,邮件推送开通后会同步发送",
"synced": synced,
}
_jload()
for n in _j.get("newsletter") or []:
if n["email"] == email:
return {"ok": True, "message": "已订阅", "synced": synced}
_j.setdefault("newsletter", []).append({"email": email, "source": source, "created_at": _now()})
_jsave()
return {
"ok": True,
"message": "订阅成功" if synced else "已登记,邮件推送开通后会同步发送",
"synced": synced,
}
def get_notif_prefs(user_id: str) -> dict:
default = {
"email": True,
"push": False,
"match": True,
"meetup": True,
"community": True,
"marketing": False,
"quiet_hours": "",
}
if use_pb():
row = safe_first("notification_preferences", filter=f"userId={q(user_id)}")
if not row:
return default
channels = row.get("channels") or {}
return {**default, **channels}
_jload()
return _j.get("notif_prefs", {}).get(user_id) or default
def set_notif_prefs(user_id: str, prefs: dict) -> dict:
merged = {**get_notif_prefs(user_id), **prefs}
if use_pb():
row = safe_first("notification_preferences", filter=f"userId={q(user_id)}")
if row:
safe_update("notification_preferences", row["id"], {"channels": merged, "userId": user_id})
else:
safe_create("notification_preferences", {"userId": user_id, "channels": merged})
return merged
_jload()
_j.setdefault("notif_prefs", {})[user_id] = merged
_jsave()
return merged