import { serve } from "https://deno.land/std@0.190.0/http/server.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", }; interface GoogleOAuthConfig { client_id: string; client_secret: string; refresh_token: string; access_token?: string; expires_at?: number; // Timestamp when access_token expires } interface CreateMeetRequest { slot_id: string; date: string; start_time: string; end_time: string; client_name: string; client_email: string; topic: string; notes?: string; } // Function to get access token from refresh token (OAuth2) async function getGoogleAccessToken(oauthConfig: GoogleOAuthConfig): Promise<{ access_token: string; expires_in: number }> { try { console.log("Attempting to exchange refresh token for access token..."); console.log("Client ID:", oauthConfig.client_id); const tokenRequest = { client_id: oauthConfig.client_id, client_secret: oauthConfig.client_secret, refresh_token: oauthConfig.refresh_token, grant_type: "refresh_token", }; console.log("Token request payload:", JSON.stringify({ ...tokenRequest, client_secret: "***REDACTED***", refresh_token: tokenRequest.refresh_token.substring(0, 20) + "..." })); const response = await fetch("https://oauth2.googleapis.com/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(tokenRequest), }); console.log("Token response status:", response.status); if (!response.ok) { const errorText = await response.text(); console.error("Token response error:", errorText); throw new Error(`Token exchange failed: ${errorText}`); } const data = await response.json(); console.log("Token response data:", JSON.stringify(data, null, 2)); if (!data.access_token) { throw new Error("No access token in response"); } console.log("Successfully obtained access token"); return { access_token: data.access_token, expires_in: data.expires_in || 3600 }; } catch (error: any) { console.error("Error getting Google access token:", error); throw error; } } serve(async (req: Request): Promise => { if (req.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } const logs: string[] = []; const log = (msg: string) => { console.log(msg); logs.push(msg); }; try { const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const supabase = createClient(supabaseUrl, supabaseServiceKey); // Clone the request to avoid stream consumption issues // Read body text first, then parse JSON let body: CreateMeetRequest; let debugInfo: any = { method: req.method, headers: Object.fromEntries(req.headers.entries()), contentType: req.headers.get("content-type"), bodyConsumed: false }; try { log("Starting to read request body..."); debugInfo.bodyReadAttempt = "Starting req.text()"; const bodyText = await req.text(); debugInfo.bodyLength = bodyText.length; debugInfo.bodyPreview = bodyText.substring(0, 200); log(`Raw body text: ${bodyText.substring(0, 100)}...`); body = JSON.parse(bodyText); debugInfo.parsedBody = body; log(`Parsed body: ${JSON.stringify(body)}`); } catch (bodyError) { debugInfo.readError = (bodyError as Error).message; log(`Error reading body: ${(bodyError as Error).message}`); log(`Debug info: ${JSON.stringify(debugInfo, null, 2)}`); return new Response( JSON.stringify({ success: false, message: "Invalid request body: " + (bodyError as Error).message, debug: debugInfo, logs: logs }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } log(`Creating Google Meet event for slot: ${body.slot_id}`); // Get platform settings log("Fetching platform settings..."); const { data: settings, error: settingsError } = await supabase .from("platform_settings") .select("integration_google_calendar_id, google_oauth_config") .single(); if (settingsError) { log(`Error fetching settings: ${JSON.stringify(settingsError)}`); return new Response( JSON.stringify({ success: false, message: "Error fetching settings: " + settingsError.message, logs: logs }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } const calendarId = settings?.integration_google_calendar_id; log(`Calendar ID: ${calendarId}`); if (!calendarId) { log("Calendar ID not configured"); return new Response( JSON.stringify({ success: false, message: "Google Calendar ID belum dikonfigurasi di Pengaturan > Integrasi", logs: logs }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Get OAuth config from settings const oauthConfigJson = settings?.google_oauth_config; if (!oauthConfigJson) { log("OAuth config not found"); return new Response( JSON.stringify({ success: false, message: "Google OAuth Config belum dikonfigurasi. Tambahkan di Pengaturan > Integrasi. Format: {\"client_id\":\"...\",\"client_secret\":\"...\",\"refresh_token\":\"...\"}", logs: logs }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Parse OAuth config JSON let oauthConfig: GoogleOAuthConfig; try { oauthConfig = JSON.parse(oauthConfigJson); log("OAuth config parsed successfully"); } catch (error: any) { log(`Failed to parse OAuth config: ${error.message}`); return new Response( JSON.stringify({ success: false, message: "Format Google OAuth Config tidak valid: " + error.message, logs: logs }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } // Check if we have a valid cached access_token let accessToken: string; const now = Math.floor(Date.now() / 1000); // Current time in seconds if (oauthConfig.access_token && oauthConfig.expires_at && oauthConfig.expires_at > now + 60) { // Token is still valid (with 60 second buffer) log(`Using cached access_token (expires at: ${new Date(oauthConfig.expires_at * 1000).toISOString()})`); accessToken = oauthConfig.access_token; } else { // Need to refresh the token log("Access token expired or missing, refreshing..."); const tokenData = await getGoogleAccessToken(oauthConfig); accessToken = tokenData.access_token; // Update the cached token in database with new expiry const newExpiresAt = now + tokenData.expires_in; const updatedConfig = { ...oauthConfig, access_token: accessToken, expires_at: newExpiresAt }; // Save updated config back to database await supabase .from("platform_settings") .update({ google_oauth_config: JSON.stringify(updatedConfig) }) .eq("id", settings.id); log("Updated cached access_token in database"); } log("Got access token"); // Build event data // Include +07:00 timezone offset to ensure times are treated as Asia/Jakarta time const startDate = new Date(`${body.date}T${body.start_time}+07:00`); const endDate = new Date(`${body.date}T${body.end_time}+07:00`); log(`Event time: ${startDate.toISOString()} to ${endDate.toISOString()}`); const eventData = { summary: `Konsultasi: ${body.topic} - ${body.client_name}`, description: `Kategori: ${body.topic}\n\nClient: ${body.client_email}\n\nCatatan: ${body.notes || '-'}\n\nSession ID: ${body.slot_id}`, start: { dateTime: startDate.toISOString(), timeZone: "Asia/Jakarta", }, end: { dateTime: endDate.toISOString(), timeZone: "Asia/Jakarta", }, conferenceData: { createRequest: { requestId: body.slot_id, conferenceSolutionKey: { type: "hangoutsMeet" } }, }, }; log(`Creating event in calendar: ${calendarId}`); log(`Event data: ${JSON.stringify(eventData, null, 2)}`); // Create event via Google Calendar API with better error handling let calendarResponse: Response; try { log("Calling Google Calendar API..."); calendarResponse = await fetch( `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?conferenceDataVersion=1`, { method: "POST", headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": "Deno/1.0 (Supabase Edge Function)", }, body: JSON.stringify(eventData), } ); } catch (fetchError: any) { log(`Network error calling Google Calendar API: ${fetchError.message}`); return new Response( JSON.stringify({ success: false, message: "Network error calling Google Calendar API: " + fetchError.message, logs: logs }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } log(`Calendar API response status: ${calendarResponse.status}`); if (!calendarResponse.ok) { const errorText = await calendarResponse.text(); log(`Google Calendar API error: ${errorText}`); return new Response( JSON.stringify({ success: false, message: "Gagal membuat event di Google Calendar: " + errorText, logs: logs }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } const eventDataResult = await calendarResponse.json(); log(`Event created with ID: ${eventDataResult.id}`); log(`Full event response: ${JSON.stringify(eventDataResult, null, 2)}`); // Check if conference data was created if (eventDataResult.conferenceData && eventDataResult.conferenceData.entryPoints) { const meetLink = eventDataResult.conferenceData.entryPoints.find((ep: any) => ep.entryPointType === "video")?.uri; if (meetLink) { log(`Meet link found: ${meetLink}`); // Update consulting_sessions with meet_link and event_id log(`Updating session ${body.slot_id} with meet_link and calendar_event_id`); await supabase .from("consulting_sessions") .update({ meet_link: meetLink, calendar_event_id: eventDataResult.id }) .eq("id", body.slot_id); log("Successfully completed"); return new Response( JSON.stringify({ success: true, meet_link: meetLink, event_id: eventDataResult.id, html_link: eventDataResult.htmlLink, logs: logs }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } } // Fallback to hangoutLink for backwards compatibility if (eventDataResult.hangoutLink) { log(`Using hangoutLink: ${eventDataResult.hangoutLink}`); // Update consulting_sessions with meet_link and event_id log(`Updating session ${body.slot_id} with meet_link and calendar_event_id`); await supabase .from("consulting_sessions") .update({ meet_link: eventDataResult.hangoutLink, calendar_event_id: eventDataResult.id }) .eq("id", body.slot_id); log("Successfully completed"); return new Response( JSON.stringify({ success: true, meet_link: eventDataResult.hangoutLink, event_id: eventDataResult.id, html_link: eventDataResult.htmlLink, logs: logs }), { headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } log("Event created but no meet link found"); return new Response( JSON.stringify({ success: false, message: "Event berhasil dibuat tapi tidak ada meet link", logs: logs }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } catch (error: any) { log(`Error creating Google Meet event: ${error.message}`); log(`Stack: ${error.stack}`); return new Response( JSON.stringify({ success: false, message: error.message || "Unknown error occurred", logs: logs }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } ); } });