/** * POST /publish-comment * * Design intent: * - Conversation requires consent (mutual follow). * - Sharp speech is rejected quietly. * - Comments never affect post reach. * * Flow: * 1. Validate auth and inputs * 2. Verify mutual follow with post author * 3. Reject profanity or hostility * 4. Store comment with tone metadata * 5. Log audit event */ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts'; import { analyzeTone, getRewriteSuggestion } from '../_shared/tone-detection.ts'; import { validateCommentBody, validateUUID, ValidationError } from '../_shared/validation.ts'; interface PublishCommentRequest { post_id: string; body: string; } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }, }); } try { // 1. Validate auth const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response(JSON.stringify({ error: 'Missing authorization header' }), { status: 401, headers: { '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: { 'Content-Type': 'application/json' }, }); } // 2. Parse request const { post_id, body } = (await req.json()) as PublishCommentRequest; // 3. Validate inputs validateUUID(post_id, 'post_id'); validateCommentBody(body); // 4. Get post author const { data: post, error: postError } = await supabase .from('posts') .select('author_id') .eq('id', post_id) .single(); if (postError || !post) { return new Response( JSON.stringify({ error: 'Post not found', message: 'This post does not exist or you cannot see it.', }), { status: 404, headers: { 'Content-Type': 'application/json' }, } ); } // 5. Verify mutual follow const serviceClient = createServiceClient(); const { data: isMutual, error: followError } = await serviceClient.rpc('is_mutual_follow', { user_a: user.id, user_b: post.author_id, }); if (followError) { console.error('Error checking mutual follow:', followError); return new Response(JSON.stringify({ error: 'Failed to verify follow relationship' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } if (!isMutual) { return new Response( JSON.stringify({ error: 'Mutual follow required', message: 'You can only comment on posts from people you mutually follow.', }), { status: 403, headers: { 'Content-Type': 'application/json' }, } ); } // 6. Analyze tone const analysis = analyzeTone(body); // 7. Reject hostile or profane content if (analysis.shouldReject) { await serviceClient.rpc('log_audit_event', { p_actor_id: user.id, p_event_type: 'comment_rejected', p_payload: { post_id, tone: analysis.tone, cis: analysis.cis, flags: analysis.flags, reason: analysis.rejectReason, }, }); return new Response( JSON.stringify({ error: 'Comment rejected', message: analysis.rejectReason, suggestion: getRewriteSuggestion(analysis), tone: analysis.tone, }), { status: 400, headers: { 'Content-Type': 'application/json' }, } ); } // 8. Create comment const { data: comment, error: commentError } = await supabase .from('comments') .insert({ post_id, author_id: user.id, body, tone_label: analysis.tone, status: 'active', }) .select() .single(); if (commentError) { console.error('Error creating comment:', commentError); return new Response(JSON.stringify({ error: 'Failed to create comment' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } // 9. Log successful comment await serviceClient.rpc('log_audit_event', { p_actor_id: user.id, p_event_type: 'comment_created', p_payload: { comment_id: comment.id, post_id, tone: analysis.tone, cis: analysis.cis, }, }); // 10. Return comment return new Response(JSON.stringify({ comment }), { status: 201, headers: { 'Content-Type': 'application/json' }, }); } catch (error) { if (error instanceof ValidationError) { return new Response( JSON.stringify({ error: 'Validation error', message: error.message, field: error.field, }), { status: 400, headers: { 'Content-Type': 'application/json' }, } ); } console.error('Unexpected error:', error); return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } });