import "@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; const APIFY_BASE = "https://api.apify.com/v2"; interface EnrichRequest { reportId: string; clinicName: string; instagramHandle?: string; youtubeChannelId?: string; address?: string; } async function runApifyActor( actorId: string, input: Record, token: string ): Promise { const res = await fetch( `${APIFY_BASE}/acts/${actorId}/runs?token=${token}&waitForFinish=120`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), } ); const run = await res.json(); const datasetId = run.data?.defaultDatasetId; if (!datasetId) return []; const itemsRes = await fetch( `${APIFY_BASE}/datasets/${datasetId}/items?token=${token}&limit=20` ); return itemsRes.json(); } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: corsHeaders }); } try { const { reportId, clinicName, instagramHandle, youtubeChannelId, address } = (await req.json()) as EnrichRequest; const APIFY_TOKEN = Deno.env.get("APIFY_API_TOKEN"); if (!APIFY_TOKEN) throw new Error("APIFY_API_TOKEN not configured"); const enrichment: Record = {}; // Run all enrichment tasks in parallel const tasks = []; // 1. Instagram Profile if (instagramHandle) { tasks.push( (async () => { const items = await runApifyActor( "apify~instagram-profile-scraper", { usernames: [instagramHandle], resultsLimit: 12 }, APIFY_TOKEN ); const profile = (items as Record[])[0]; if (profile && !profile.error) { enrichment.instagram = { username: profile.username, followers: profile.followersCount, following: profile.followsCount, posts: profile.postsCount, bio: profile.biography, isBusinessAccount: profile.isBusinessAccount, externalUrl: profile.externalUrl, latestPosts: ((profile.latestPosts as Record[]) || []) .slice(0, 12) .map((p) => ({ type: p.type, likes: p.likesCount, comments: p.commentsCount, caption: (p.caption as string || "").slice(0, 200), timestamp: p.timestamp, })), }; } })() ); } // 2. Google Maps / Place Reviews if (clinicName || address) { tasks.push( (async () => { const searchQuery = `${clinicName} ${address || "강남"}`; const items = await runApifyActor( "compass~crawler-google-places", { searchStringsArray: [searchQuery], maxCrawledPlacesPerSearch: 1, language: "ko", maxReviews: 10, }, APIFY_TOKEN ); const place = (items as Record[])[0]; if (place) { enrichment.googleMaps = { name: place.title, rating: place.totalScore, reviewCount: place.reviewsCount, address: place.address, phone: place.phone, website: place.website, category: place.categoryName, openingHours: place.openingHours, topReviews: ((place.reviews as Record[]) || []) .slice(0, 10) .map((r) => ({ stars: r.stars, text: (r.text as string || "").slice(0, 200), publishedAtDate: r.publishedAtDate, })), }; } })() ); } // 3. YouTube Channel (using YouTube Data API v3) if (youtubeChannelId) { const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY"); if (YOUTUBE_API_KEY) { tasks.push( (async () => { const YT_BASE = "https://www.googleapis.com/youtube/v3"; // Resolve handle/username to channel ID let channelId = youtubeChannelId; if (channelId.startsWith("@") || !channelId.startsWith("UC")) { // Use forHandle for @handles, forUsername for legacy usernames const param = channelId.startsWith("@") ? "forHandle" : "forUsername"; const handle = channelId.startsWith("@") ? channelId.slice(1) : channelId; const lookupRes = await fetch( `${YT_BASE}/channels?part=id&${param}=${handle}&key=${YOUTUBE_API_KEY}` ); const lookupData = await lookupRes.json(); channelId = lookupData.items?.[0]?.id || ""; } if (!channelId) return; // Step 1: Get channel statistics & snippet (1 quota unit) const channelRes = await fetch( `${YT_BASE}/channels?part=snippet,statistics,brandingSettings&id=${channelId}&key=${YOUTUBE_API_KEY}` ); const channelData = await channelRes.json(); const channel = channelData.items?.[0]; if (!channel) return; const stats = channel.statistics || {}; const snippet = channel.snippet || {}; // Step 2: Get recent/popular videos (100 quota units) const searchRes = await fetch( `${YT_BASE}/search?part=snippet&channelId=${channelId}&order=viewCount&type=video&maxResults=10&key=${YOUTUBE_API_KEY}` ); const searchData = await searchRes.json(); const videoIds = (searchData.items || []) .map((item: Record) => (item.id as Record)?.videoId) .filter(Boolean) .join(","); // Step 3: Get video details — views, likes, duration (1 quota unit) let videos: Record[] = []; if (videoIds) { const videosRes = await fetch( `${YT_BASE}/videos?part=snippet,statistics,contentDetails&id=${videoIds}&key=${YOUTUBE_API_KEY}` ); const videosData = await videosRes.json(); videos = videosData.items || []; } enrichment.youtube = { channelId, channelName: snippet.title, handle: snippet.customUrl || youtubeChannelId, description: snippet.description?.slice(0, 500), publishedAt: snippet.publishedAt, thumbnailUrl: snippet.thumbnails?.default?.url, subscribers: parseInt(stats.subscriberCount || "0", 10), totalViews: parseInt(stats.viewCount || "0", 10), totalVideos: parseInt(stats.videoCount || "0", 10), videos: videos.slice(0, 10).map((v) => { const vs = v.statistics as Record || {}; const vSnippet = v.snippet as Record || {}; const vContent = v.contentDetails as Record || {}; return { title: vSnippet.title, views: parseInt(vs.viewCount || "0", 10), likes: parseInt(vs.likeCount || "0", 10), comments: parseInt(vs.commentCount || "0", 10), date: vSnippet.publishedAt, duration: vContent.duration, url: `https://www.youtube.com/watch?v=${(v.id as string)}`, thumbnail: (vSnippet.thumbnails as Record>)?.medium?.url, }; }), }; })() ); } } await Promise.allSettled(tasks); // Save enrichment data to Supabase const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const supabase = createClient(supabaseUrl, supabaseKey); if (reportId) { // Get existing report const { data: existing } = await supabase .from("marketing_reports") .select("report") .eq("id", reportId) .single(); if (existing) { const updatedReport = { ...existing.report, channelEnrichment: enrichment, enrichedAt: new Date().toISOString(), }; await supabase .from("marketing_reports") .update({ report: updatedReport, updated_at: new Date().toISOString() }) .eq("id", reportId); } } return new Response( JSON.stringify({ success: true, data: enrichment, enrichedAt: new Date().toISOString(), }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error) { return new Response( JSON.stringify({ success: false, error: error.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } });