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>
205 lines
6.1 KiB
Python
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
|