File size: 13,731 Bytes
41b754a
 
 
 
6a5e36e
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a5e36e
 
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
 
 
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
41b754a
 
 
 
 
 
a65b2f8
41b754a
 
 
 
 
 
 
 
a65b2f8
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
41b754a
 
a65b2f8
 
41b754a
 
 
a65b2f8
41b754a
a65b2f8
41b754a
 
 
 
 
a65b2f8
41b754a
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
 
 
 
 
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
41b754a
 
 
a65b2f8
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a65b2f8
 
 
41b754a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""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