backend / engine_story.py
muhammadpriv001's picture
Refactor: Conversation deletion logic and admin button fix
4f5ecd1
Raw
History Blame Contribute Delete
8.03 kB
"""Story engine — CRUD + visibility logic for 24-hour stories."""
from datetime import datetime, timezone
from db import get_service_client
import engine_user
def _now_iso():
return datetime.now(timezone.utc).isoformat()
def create_story(username: str, media_path: str, media_type: str, station_name: str = "", caption: str = "") -> dict:
from db import reset_service_client
for attempt in range(2):
try:
sb = get_service_client()
row = sb.table("stories").insert({
"username": username,
"station_name": station_name,
"media_path": media_path,
"media_type": media_type,
"caption": caption,
}).execute().data[0]
return row
except Exception as e:
if attempt == 0 and "ConnectError" in type(e).__name__ or "10054" in str(e) or "forcibly closed" in str(e):
reset_service_client()
continue
raise
def delete_story(story_id: int, username: str) -> bool:
sb = get_service_client()
# Allow author OR station admin
rows = sb.table("stories").select("id, username, station_name").eq("id", story_id).execute()
if not rows.data:
return False
story = rows.data[0]
is_author = story["username"] == username
is_admin = False
if story.get("station_name"):
admin_rows = sb.table("stations").select("admin").eq("station_name", story["station_name"]).execute()
is_admin = bool(admin_rows.data) and admin_rows.data[0]["admin"] == username
if not is_author and not is_admin:
return False
sb.table("stories").delete().eq("id", story_id).execute()
return True
def get_story_views(story_id: int, requester: str) -> list[dict]:
"""Return viewers for a story. Only author or station admin can see."""
sb = get_service_client()
rows = sb.table("stories").select("username, station_name").eq("id", story_id).execute()
if not rows.data:
return []
story = rows.data[0]
is_author = story["username"] == requester
is_admin = False
if story.get("station_name"):
admin_rows = sb.table("stations").select("admin").eq("station_name", story["station_name"]).execute()
is_admin = bool(admin_rows.data) and admin_rows.data[0]["admin"] == requester
if not is_author and not is_admin:
return []
view_rows = sb.table("story_views").select("viewer, viewed_at").eq("story_id", story_id).execute()
# Fetch avatars
viewers = [r["viewer"] for r in view_rows.data]
avatar_map = {}
if viewers:
u_rows = sb.table("users").select("username, avatar").in_("username", viewers).execute()
avatar_map = {r["username"]: r.get("avatar", "") for r in u_rows.data}
return [{"username": r["viewer"], "avatar": avatar_map.get(r["viewer"], ""), "viewed_at": r["viewed_at"]} for r in view_rows.data if r["viewer"] != requester]
def _is_follower(viewer: str, owner: str) -> bool:
if not viewer or not owner:
return False
sb = get_service_client()
rows = sb.table("follows").select("status").eq("follower", viewer).eq("following", owner).execute()
return bool(rows.data) and rows.data[0]["status"] == 1
def _user_visibility(username: str) -> int:
"""0 = public, 1 = private"""
sb = get_service_client()
rows = sb.table("users").select("visibility").eq("username", username).execute()
if not rows.data:
return 0
return int(rows.data[0].get("visibility") or 0)
def _with_retry(fn):
"""Call fn(sb), reset client and retry once on connection errors."""
from db import reset_service_client
for attempt in range(2):
try:
return fn(get_service_client())
except Exception as e:
if attempt == 0 and ("ConnectError" in type(e).__name__ or "10054" in str(e) or "forcibly closed" in str(e)):
reset_service_client()
continue
raise
def _fetch_stories(sb):
return sb.table("stories").select(
"id, username, station_name, media_path, media_type, caption, created_at, expires_at"
).gt("expires_at", _now_iso()).order("created_at", desc=False).execute().data
def get_user_stories(viewer: str) -> list[dict]:
"""Return active user stories (station_name='') visible to viewer."""
sb = get_service_client()
all_rows = _with_retry(_fetch_stories)
rows = [r for r in all_rows if r.get("station_name", "") == ""]
# Get viewer's following list for private-account filtering
following_set: set[str] = set()
if viewer:
f_rows = sb.table("follows").select("following").eq("follower", viewer).eq("status", 1).execute()
following_set = {r["following"] for r in f_rows.data}
# Get visibility map for all unique usernames
usernames = list({r["username"] for r in rows})
vis_map: dict[str, int] = {}
if usernames:
u_rows = sb.table("users").select("username, visibility").in_("username", usernames).execute()
vis_map = {r["username"]: int(r.get("visibility") if r.get("visibility") is not None else 1) for r in u_rows.data}
# Get viewed story ids for viewer
viewed_ids: set[int] = set()
if viewer:
v_rows = sb.table("story_views").select("story_id").eq("viewer", viewer).execute()
viewed_ids = {r["story_id"] for r in v_rows.data}
# Get avatars
avatar_map: dict[str, str] = {}
if usernames:
a_rows = sb.table("users").select("username, avatar").in_("username", usernames).execute()
avatar_map = {r["username"]: (r.get("avatar") or "") for r in a_rows.data}
result = []
for r in rows:
owner = r["username"]
if owner == viewer:
r["viewed"] = r["id"] in viewed_ids
r["avatar"] = avatar_map.get(owner, "")
result.append(r)
continue
# Visibility check: 0=private, 1=public
vis = vis_map.get(owner, 1)
if vis == 0 and owner not in following_set:
continue
# Block check
if viewer and engine_user.is_blocked_either_way(viewer, owner):
continue
r["viewed"] = r["id"] in viewed_ids
r["avatar"] = avatar_map.get(owner, "")
result.append(r)
return result
def get_station_stories(viewer: str, station_names: list[str]) -> list[dict]:
"""Return active station stories for given stations visible to viewer."""
if not station_names:
return []
sb = get_service_client()
all_rows = _with_retry(_fetch_stories)
rows = [r for r in all_rows if r.get("station_name", "") in station_names and r.get("station_name", "") != ""]
# Get viewed story ids
viewed_ids: set[int] = set()
if viewer:
v_rows = sb.table("story_views").select("story_id").eq("viewer", viewer).execute()
viewed_ids = {r["story_id"] for r in v_rows.data}
# Get avatars for story authors
usernames = list({r["username"] for r in rows})
avatar_map: dict[str, str] = {}
if usernames:
a_rows = sb.table("users").select("username, avatar").in_("username", usernames).execute()
avatar_map = {r["username"]: (r.get("avatar") or "") for r in a_rows.data}
# Get station profile pics
station_pic_map: dict[str, str] = {}
if station_names:
s_rows = sb.table("stations").select("station_name, profile_pic").in_("station_name", station_names).execute()
station_pic_map = {r["station_name"]: (r.get("profile_pic") or "") for r in s_rows.data}
result = []
for r in rows:
r["viewed"] = r["id"] in viewed_ids
r["avatar"] = avatar_map.get(r["username"], "")
r["station_pic"] = station_pic_map.get(r["station_name"], "")
result.append(r)
return result
def mark_viewed(story_id: int, viewer: str) -> None:
if not viewer:
return
sb = get_service_client()
sb.table("story_views").upsert({"story_id": story_id, "viewer": viewer}).execute()