backend / engine_content.py
muhammadpriv001's picture
Deploying YapStation Backend
6a5e36e
Raw
History Blame Contribute Delete
13.7 kB
"""Content module"""
import time
from db import get_service_client
import engine_user
from engineHelper import parse_db_timestamp, upload_media_to_cloudinary
import threading
# In-memory caches (simple dict caches)
_avatar_cache = {} # username -> (avatar_url, timestamp)
_post_likes_cache = {} # post_id -> (likes_count, timestamp)
_cache_ttl = 60 # seconds
_cache_lock = threading.Lock()
def _get_cached(cache, key, ttl=None):
ttl = ttl or _cache_ttl
with _cache_lock:
if key in cache:
value, ts = cache[key]
if time.time() - ts < ttl:
return value
del cache[key]
return None
def _set_cache(cache, key, value):
with _cache_lock:
cache[key] = (value, time.time())
# ========================
# POSTS
# ========================
def create_post(username, content, media_path):
sb = get_service_client()
if media_path and media_path.startswith("data:"):
media_path = upload_media_to_cloudinary(media_path)
result = sb.table("posts").insert({
"username": username, "station_name": "", "content": content,
"media_path": media_path, "comments_count": 0, "likes_count": 0
}).execute()
row = result.data[0]
avatar = engine_user.get_avatar(username)
return {
"id": str(row["id"]), "username": username, "content": content,
"media_path": media_path, "timestamp": str(row["timestamp"]), "comments_count": "0",
"likes_count": "0", "station_name": "", "avatar": avatar
}
def delete_post(post_id, username):
sb = get_service_client()
# Check ownership
rows = sb.table("posts").select("id").eq("id", post_id).eq("username", username).execute()
if not rows.data: return False
# Cascading deletes handle comments and likes!
sb.table("posts").delete().eq("id", post_id).eq("username", username).execute()
return True
def get_feed(viewer=""):
sb = get_service_client()
rows = sb.table("posts").select("id, username, content, media_path, timestamp, comments_count, likes_count, station_name").order("timestamp", desc=True).execute()
# Get blocking set
restricted_users = engine_user.get_blocking_relationship_usernames(viewer) if viewer else set()
# Normalize restricted users to lowercase for faster case-insensitive lookup
restricted_users_low = {u.lower() for u in restricted_users if u}
viewer_low = viewer.lower() if viewer else ""
# Get station admins for station posts filtering
unique_stations = list(set(r["station_name"] for r in rows.data if r.get("station_name")))
station_admin_map = {}
if unique_stations:
try:
import engine_station
stations_res = sb.table("stations").select("station_name, admin, profile_pic").in_("station_name", unique_stations).execute()
for sr in stations_res.data:
station_admin_map[sr["station_name"]] = (sr["admin"], sr["profile_pic"] or "")
except:
pass
results = []
# Pre-fetch likes if viewer is provided
liked_post_ids = []
if viewer:
likes_res = sb.table("likes").select("post_id").eq("username", viewer).execute()
liked_post_ids = [str(l["post_id"]) for l in likes_res.data]
# Performance optimization: pre-fetch visibility and follow status for all unique users
unique_users = list(set(r["username"] for r in rows.data if r.get("username") and not r.get("station_name")))
visibility_map = {}
following_set_low = set()
if unique_users:
try:
users_res = sb.table("users").select("username, visibility").in_("username", unique_users).execute()
for ur in users_res.data:
visibility_map[ur["username"]] = int(ur["visibility"])
if viewer:
follows_res = sb.table("follows").select("following").eq("follower", viewer).eq("status", 1).in_("following", unique_users).execute()
following_set_low = {f["following"].lower() for f in follows_res.data}
except:
pass
avatar_map = {}
if unique_users:
# Check cache first
missing_users = []
for u in unique_users:
cached = _get_cached(_avatar_cache, u)
if cached is not None:
avatar_map[u] = cached
else:
missing_users.append(u)
if missing_users:
try:
# Batch fetch missing avatars from DB
users_res = sb.table("users").select("username, avatar").in_("username", missing_users).execute()
for ur in users_res.data:
u_name = ur.get("username")
if u_name:
av = ur.get("avatar") or ""
avatar_map[u_name] = av
_set_cache(_avatar_cache, u_name, av)
except:
pass
for r in rows.data:
# Filtering based on blocks
post_user = r["username"]
post_user_low = post_user.lower() if post_user else ""
station_name = r["station_name"]
# If user is blocked/blocker, skip unless it's the viewer themselves
if post_user_low in restricted_users_low and post_user_low != viewer_low:
continue
# Private account check (only for personal posts)
if not station_name and post_user_low != viewer_low:
visibility = visibility_map.get(post_user, 1) # Default to public if missing
if visibility == 0 and post_user_low not in following_set_low:
continue
# If it's a station post, check station admin
if station_name:
admin, s_avatar = station_admin_map.get(station_name, (None, ""))
if admin and admin.lower() in restricted_users_low and admin.lower() != viewer_low:
continue
r["avatar"] = s_avatar
else:
r["avatar"] = avatar_map.get(post_user, "")
p = {k: str(v) for k, v in r.items()}
p["timestamp"] = str(parse_db_timestamp(r["timestamp"]))
p["is_liked"] = p["id"] in liked_post_ids
results.append(p)
return results
def get_user_posts(username, viewer=""):
# If blocked, return empty
if viewer and engine_user.is_blocked_either_way(username, viewer):
return []
sb = get_service_client()
# Check target visibility
if username != viewer:
try:
target_res = sb.table("users").select("visibility").eq("username", username).execute()
if target_res.data:
visibility = int(target_res.data[0]["visibility"])
if visibility == 0:
# Check if viewer follows
status = engine_user.get_follow_status(viewer, username)
if status != 1:
return []
except:
pass
# Case-insensitive self-check: viewer can always see their own posts
is_self = False
if viewer and username and viewer.lower() == username.lower():
is_self = True
rows = sb.table("posts").select("id, username, station_name, content, media_path, timestamp, comments_count, likes_count").eq("username", username).order("timestamp", desc=True).execute()
# Pre-fetch likes if viewer is provided
liked_post_ids = []
if viewer:
try:
likes_res = sb.table("likes").select("post_id").eq("username", viewer).eq("isLike", 1).execute()
liked_post_ids = [str(l["post_id"]) for l in likes_res.data]
except:
pass
# Batch fetch station avatars if any posts are in stations
unique_stations = list(set(r["station_name"] for r in rows.data if r.get("station_name")))
station_avatar_map = {}
if unique_stations:
try:
stations_res = sb.table("stations").select("station_name, profile_pic").in_("station_name", unique_stations).execute()
for sr in stations_res.data:
s_name = sr.get("station_name")
if s_name:
station_avatar_map[s_name] = sr.get("profile_pic") or ""
except:
pass
# Fetch user avatar
cached_avatar = _get_cached(_avatar_cache, username)
if cached_avatar is None:
cached_avatar = engine_user.get_avatar(username)
_set_cache(_avatar_cache, username, cached_avatar)
user_avatar = cached_avatar
results = []
for r in rows.data:
p = {k: str(v) for k, v in r.items()}
p["timestamp"] = str(parse_db_timestamp(r["timestamp"]))
p["is_liked"] = p["id"] in liked_post_ids
# If it's a station post, use station avatar
if r.get("station_name"):
p["avatar"] = station_avatar_map.get(r["station_name"], "")
else:
p["avatar"] = user_avatar
results.append(p)
return results
def is_liked(post_id, username):
sb = get_service_client()
res = sb.table("likes").select("isLike").eq("post_id", post_id).eq("username", username).execute()
if res.data:
return str(res.data[0]["isLike"]) == "1"
return False
# ========================
# LIKES
# ========================
def like_post(post_id, username):
sb = get_service_client()
pid = post_id
rows = sb.table("likes").select("isLike").eq("post_id", pid).eq("username", username).execute()
if rows.data:
if str(rows.data[0]["isLike"]) == "1":
return False
sb.table("likes").update({"isLike": 1}).eq("post_id", pid).eq("username", username).execute()
return True
sb.table("likes").insert({"post_id": pid, "username": username, "isLike": 1}).execute()
return True
def unlike_post(post_id, username):
sb = get_service_client()
pid = post_id
rows = sb.table("likes").select("isLike").eq("post_id", pid).eq("username", username).execute()
if not rows.data: return False
if str(rows.data[0]["isLike"]) == "0":
return False
sb.table("likes").update({"isLike": 0}).eq("post_id", pid).eq("username", username).execute()
return True
def modify_likes(post_id, delta):
sb = get_service_client()
rows = sb.table("posts").select("likes_count").eq("id", post_id).execute()
if not rows.data: return -1
current = int(rows.data[0]["likes_count"])
new_count = max(0, current + delta)
sb.table("posts").update({"likes_count": new_count}).eq("id", post_id).execute()
# Update cache
_set_cache(_post_likes_cache, post_id, new_count)
return new_count
# ========================
# COMMENTS
# ========================
def add_comment(post_id, username, content):
sb = get_service_client()
# Database handles timestamp automatically via DEFAULT NOW()
sb.table("comments").insert({"post_id": post_id, "username": username, "content": content}).execute()
rows = sb.table("posts").select("comments_count").eq("id", post_id).execute()
if rows.data:
sb.table("posts").update({"comments_count": int(rows.data[0]["comments_count"]) + 1}).eq("id", post_id).execute()
return True
def get_comments(post_id, viewer=""):
sb = get_service_client()
rows = sb.table("comments").select("username, content, timestamp").eq("post_id", post_id).order("timestamp", desc=True).execute()
restricted_users = {u.lower() for u in engine_user.get_blocking_relationship_usernames(viewer)} if viewer else set()
results = []
for r in rows.data:
if r["username"].lower() in restricted_users:
continue
results.append({
"username": r["username"],
"content": r["content"],
"timestamp": str(parse_db_timestamp(r["timestamp"]))
})
return results
def get_post(post_id, viewer=""):
sb = get_service_client()
rows = sb.table("posts").select("*").eq("id", post_id).execute()
if not rows.data:
return None
r = rows.data[0]
# Check blocks
if viewer:
if engine_user.is_blocked_either_way(r["username"], viewer):
return None
if r["station_name"]:
import engine_station
admin = engine_station.get_station_admin(r["station_name"])
if admin and engine_user.is_blocked_either_way(admin, viewer):
return None
# Check visibility: if post owner is private and viewer doesn't follow them
post_owner = r["username"]
is_self = viewer and post_owner and viewer.lower() == post_owner.lower()
if viewer and not is_self and not r.get("station_name"):
owner_res = sb.table("users").select("visibility").eq("username", post_owner).execute()
if owner_res.data and int(owner_res.data[0]["visibility"]) == 0:
follows_res = sb.table("follows").select("id").eq("follower", viewer).eq("following", post_owner).eq("status", 1).execute()
if not follows_res.data:
return {"restricted": True, "username": post_owner}
p = {k: str(v) for k, v in r.items()}
p["timestamp"] = str(parse_db_timestamp(r["timestamp"]))
# Check if liked
p["is_liked"] = False
if viewer:
likes_res = sb.table("likes").select("isLike").eq("post_id", post_id).eq("username", viewer).execute()
if likes_res.data:
p["is_liked"] = str(likes_res.data[0]["isLike"]) == "1"
# Avatar resolution
if r.get("station_name"):
import engine_station
p["avatar"] = engine_station.get_station_profile_pic(r["station_name"])
else:
p["avatar"] = engine_user.get_avatar(r["username"])
return p