/// /// import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { ApifyClient } from "npm:apify-client@2.1.0"; // Define types locally since Edge Functions cannot import from the client codebase export type TrendVerdict = "POST NOW" | "WAIT" | "SKIP"; export interface TrendAnalysisResult { trendIdentifier: string; freshnessScore: number; saturationScore: number; shelfLifeEstimate: string; verdict: TrendVerdict; chartData: { day: string; velocity: number }[]; topVideos: { id: number; views: string; thumbnail: string }[]; } const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Content-Type': 'application/json', }; // Helper function to transform raw Apify data into the required format function transformApifyData(rawTrendIdentifier: string, rawData: any[]): TrendAnalysisResult { const totalVideos = rawData.length; // Placeholder logic for scores based on video count (simulating analysis) let freshness = 50; let saturation = 50; let verdict: TrendVerdict = "WAIT"; if (totalVideos > 50) { saturation = 80; freshness = 30; verdict = "SKIP"; } else if (totalVideos < 10) { saturation = 20; freshness = 90; verdict = "POST NOW"; } // Mock chart data (real velocity data requires historical scraping) const chartData = [ { day: 'Day -7', velocity: Math.floor(Math.random() * 100) }, { day: 'Day -6', velocity: Math.floor(Math.random() * 150) }, { day: 'Day -5', velocity: Math.floor(Math.random() * 200) }, { day: 'Day -4', velocity: Math.floor(Math.random() * 300) }, { day: 'Day -3', velocity: Math.floor(Math.random() * 400) }, { day: 'Day -2', velocity: Math.floor(Math.random() * 500) }, { day: 'Day -1', velocity: Math.floor(Math.random() * 600) }, ]; const topVideos = rawData.slice(0, 3).map((video: any, index: number) => ({ id: index + 1, views: video?.playCount && typeof video.playCount === 'number' ? (video.playCount / 1000000).toFixed(1) + 'M' : 'N/A', thumbnail: video?.cover || '/placeholder.svg', })); return { trendIdentifier: rawTrendIdentifier, freshnessScore: freshness, saturationScore: saturation, shelfLifeEstimate: "Requires deeper analysis", verdict: verdict, chartData: chartData, topVideos: topVideos, }; } // Helper function to clean the trend identifier for Apify function cleanTrendIdentifier(input: string): string { // Simple cleaning: remove leading #, trim whitespace let cleaned = input.trim(); if (cleaned.startsWith('#')) { cleaned = cleaned.substring(1); } // If it looks like a URL, we might need more complex parsing, // but for now, we assume the user is primarily inputting hashtags or clean text. return cleaned; } async function getTikTokTrendData(trendIdentifier: string): Promise { const apifyToken = Deno.env.get('APIFY_TOKEN'); if (!apifyToken) { throw new Error("APIFY_TOKEN environment variable is not set. Please configure it in Supabase secrets."); } const client = new ApifyClient({ token: apifyToken }); const cleanedIdentifier = cleanTrendIdentifier(trendIdentifier); if (!cleanedIdentifier) { throw new Error("Invalid trend identifier provided after cleaning."); } // We assume the trendIdentifier is a hashtag for this example. const actorId = 'apify/tiktok-scraper'; const runInput = { hashtags: [cleanedIdentifier], resultsLimit: 10, // Fetch a small sample for quick analysis }; console.log(`Starting Apify run for actor ${actorId} with input: ${cleanedIdentifier}`); let run; try { run = await client.actor(actorId).call(runInput); } catch (e) { console.error("Apify client call failed:", e); // Re-throw a clearer error if the API call itself fails throw new Error(`Apify API call failed: ${e.message}`); } // Check if run is properly initialized before accessing properties if (!run) { throw new Error("Apify actor run failed to initialize properly."); } console.log(`Apify run finished. Run ID: ${run.id}, Status: ${run.status}`); if (run.status === 'FAILED') { // This captures the specific error you saw in the logs throw new Error(`Apify actor run failed for identifier "${cleanedIdentifier}". Please check the Apify console for details on run ID: ${run.id}.`); } // Fetch items from the default dataset const { items } = await client.dataset(run.defaultDatasetId).listItems(); if (!items || items.length === 0) { console.log(`Apify returned no data for identifier: ${cleanedIdentifier}`); // If Apify returns no data, we still return a result indicating low activity return { trendIdentifier, freshnessScore: 10, saturationScore: 10, shelfLifeEstimate: "No recent activity found", verdict: "WAIT", chartData: [], topVideos: [], }; } // Transform the raw data into the required format return transformApifyData(trendIdentifier, items); } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { // // FIXME: Check for authentication (implement proper auth based on our needs) // const authHeader = req.headers.get('authorization'); // // if (!authHeader || !authHeader.startsWith('Bearer ')) { // return new Response( // JSON.stringify({ error: 'Unauthorized' }), // { status: 401, headers: corsHeaders } // ); // } // // // Here we validate the JWT token or API key // // For example, with Supabase JWT validation: // // // const token = authHeader.substring(7); // // const { data, error } = await supabase.auth.getUser(token); // // if (error || !data?.user) { // // return new Response( // // JSON.stringify({ error: 'Invalid token' }), // // { status: 401, headers: corsHeaders } // // ); // // } // END FIXME WIP // Manual authentication handling is omitted for simplicity but recommended for production const { trendIdentifier } = await req.json(); if (!trendIdentifier) { return new Response(JSON.stringify({ error: 'Missing trendIdentifier' }), { status: 400, headers: corsHeaders, }); } const analysisResult = await getTikTokTrendData(trendIdentifier); return new Response(JSON.stringify(analysisResult), { headers: corsHeaders, status: 200, }); } catch (error) { console.error("Edge Function Error:", error); return new Response(JSON.stringify({ error: error.message }), { headers: corsHeaders, status: 500, }); } });