"""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