import { ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import type { SupabaseClient } from "@supabase/supabase-js"; import type { Context, Next } from "hono"; import type { EventAndTablo } from "./types.ts"; export const generateICSFromEvents = ( events: EventAndTablo[], calendarName: string = "Planning" ) => { const formatDate = (date: string, time: string) => { // Combine date (YYYY-MM-DD) and time (HH:MM:SS) into ISO format then convert to UTC const dateTime = new Date(`${date}T${time}`); return `${dateTime.toISOString().replace(/[-:]/g, "").split(".")[0]}Z`; }; const escapeICSText = (text: string) => { return text .replace(/\\/g, "\\\\") .replace(/;/g, "\\;") .replace(/,/g, "\\,") .replace(/\n/g, "\\n") .replace(/\r/g, ""); }; const generateUID = (eventId: string) => { return `${eventId}@xtablo.com`; }; let icsContent = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//XTablo//Planning Export//EN", `X-WR-CALNAME:${escapeICSText(calendarName)}`, "X-WR-TIMEZONE:Europe/Paris", "CALSCALE:GREGORIAN", "METHOD:PUBLISH", ].join("\r\n"); events.forEach((event) => { if (!event.start_date || !event.start_time || !event.title) return; const startDateTime = formatDate(event.start_date, event.start_time); const endDateTime = event.end_time ? formatDate(event.start_date, event.end_time) : formatDate(event.start_date, event.start_time); // Default to start time if no end time const eventLines = [ "", "BEGIN:VEVENT", `UID:${generateUID(event.event_id)}`, `DTSTART:${startDateTime}`, `DTEND:${endDateTime}`, `SUMMARY:${escapeICSText(event.title)}`, `DESCRIPTION:${escapeICSText(`Tablo: ${event.tablo_name}\n${event.description || ""}`)}`, event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "", `CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, `LAST-MODIFIED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, "STATUS:CONFIRMED", "TRANSP:OPAQUE", "END:VEVENT", ].filter((line) => line !== ""); // Remove empty lines icsContent += `\r\n${eventLines.join("\r\n")}`; }); icsContent += "\r\nEND:VCALENDAR"; return icsContent; }; export const writeCalendarFileToR2 = async ( s3_client: S3Client, supabase: SupabaseClient, inputs: { token: string; tabloName: string; tablo_id: string; } ) => { const { token, tabloName, tablo_id } = inputs; const bucketName = "calendars"; const key = `${token}/${tabloName}.ics`; const { data: events, error: eventsError } = await supabase .from("events_and_tablos") .select("*") .eq("tablo_id", tablo_id); if (eventsError || !events) { throw new Error("Failed to generate events"); } const eventsData = events as EventAndTablo[]; const icsContent = generateICSFromEvents(eventsData, tabloName); await s3_client.send( new PutObjectCommand({ Bucket: bucketName, Key: key, Body: icsContent, }) ); }; export const getTabloFileNames = async (s3_client: S3Client, tabloId: string) => { const bucketName = "tablo-data"; const { Contents } = await s3_client.send( new ListObjectsV2Command({ Bucket: bucketName, Prefix: tabloId, }) ); return Contents?.map((content) => content.Key?.split("/")[1]).filter( (content) => content?.length && content.length > 0 ); }; const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => { const { data: tabloAccess, error: isMemberError } = await supabase .from("tablo_access") .select("*") .eq("tablo_id", tabloId) .eq("user_id", userId) .eq("is_active", true); if (isMemberError) { return false; } return tabloAccess?.length > 0; }; const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => { const { data: tabloAccess, error: isAdminError } = await supabase .from("tablo_access") .select("*") .eq("tablo_id", tabloId) .eq("user_id", userId) .eq("is_active", true) .eq("is_admin", true); if (isAdminError) { return false; } return tabloAccess?.length > 0; }; export const checkTabloMember = async (c: Context, next: Next) => { const supabase = c.get("supabase"); const user = c.get("user"); const tabloId = c.req.param("tabloId"); const isMember = await isTabloMember(supabase, tabloId, user.id); if (!isMember) { return c.json({ error: "You are not a member of this tablo" }, 403); } await next(); }; export const checkTabloAdmin = async (c: Context, next: Next) => { const supabase = c.get("supabase"); const user = c.get("user"); const tabloId = c.req.param("tabloId") || c.req.query("tablo_id"); const isAdmin = await isTabloAdmin(supabase, tabloId, user.id); if (!isAdmin) { return c.json({ error: "You are not an admin of this tablo" }, 403); } await next(); };