import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; import { createSupabaseClient, createServiceClient } from "../_shared/supabase-client.ts"; import { trySignR2Url } from "../_shared/r2_signer.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; const postSelect = ` id, body, author_id, category_id, tags, body_format, background_id, tone_label, cis_score, status, created_at, edited_at, expires_at, is_edited, allow_chain, chain_parent_id, image_url, video_url, thumbnail_url, duration_ms, type, visibility, pinned_at, chain_parent:posts ( id, body, created_at, author:profiles!posts_author_id_fkey ( id, handle, display_name ) ), metrics:post_metrics!post_metrics_post_id_fkey ( like_count, save_count, view_count ), author:profiles!posts_author_id_fkey ( id, handle, display_name, trust_state ( user_id, harmony_score, tier, posts_today ) ) `; serve(async (req: Request) => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } try { const authHeader = req.headers.get("Authorization"); if (!authHeader) { return new Response(JSON.stringify({ error: "Missing authorization header" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const supabase = createSupabaseClient(authHeader); const { data: { user }, error: authError, } = await supabase.auth.getUser(); if (authError || !user) { return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const url = new URL(req.url); const authorId = url.searchParams.get("author_id"); if (!authorId) { return new Response(JSON.stringify({ error: "Missing author_id" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const limit = Math.min(parseInt(url.searchParams.get("limit") || "20"), 100); const offset = parseInt(url.searchParams.get("offset") || "0"); // Use service client to bypass RLS issues const serviceClient = createServiceClient(); // Check visibility rules manually: // - User can always see their own posts // - Public posts are visible to everyone // - Followers-only posts require accepted follow // - Private posts only visible to author const isOwnProfile = authorId === user.id; // Get author's profile to check privacy settings const { data: authorProfile } = await serviceClient .from("profiles") .select("is_private, is_official") .eq("id", authorId) .single(); // Check if viewer follows the author (for followers-only posts) let hasAcceptedFollow = false; if (!isOwnProfile) { const { data: followRow } = await serviceClient .from("follows") .select("status") .eq("follower_id", user.id) .eq("following_id", authorId) .eq("status", "accepted") .maybeSingle(); hasAcceptedFollow = !!followRow; } // Build query with appropriate visibility filter // Note: posts use 'active' status for published posts let query = serviceClient .from("posts") .select(postSelect) .eq("author_id", authorId) .eq("status", "active"); // Apply visibility filters based on relationship if (isOwnProfile) { // User can see all their own posts (no visibility filter) } else if (hasAcceptedFollow) { // Follower can see public and followers-only posts query = query.in("visibility", ["public", "followers"]); } else if (authorProfile?.is_official || authorProfile?.is_private === false) { // Public/official profiles - only public posts query = query.eq("visibility", "public"); } else { // Private profile without follow - only public posts query = query.eq("visibility", "public"); } const { data: posts, error } = await query .order("pinned_at", { ascending: false, nullsFirst: false }) .order("created_at", { ascending: false }) .range(offset, offset + limit - 1); if (error) { console.error("Profile posts fetch error:", error); return new Response(JSON.stringify({ error: "Failed to fetch posts" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const signedPosts = await Promise.all( (posts || []).map(async (post: any) => { const imageUrl = post.image_url ? await trySignR2Url(post.image_url) : null; const thumbUrl = post.thumbnail_url ? await trySignR2Url(post.thumbnail_url) : null; return { ...post, image_url: imageUrl, thumbnail_url: thumbUrl, }; }) ); return new Response( JSON.stringify({ posts: signedPosts, pagination: { limit, offset, returned: signedPosts.length }, }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error) { console.error("Unexpected profile-posts error:", error); return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } });