code icon Code

Fetch Calendar Events

Fetch events for a specific date range (for queries)

Source Code

import fs from "fs";
import path from "path";

const [startDate, endDate, outputPath] = process.argv.slice(2);

if (!startDate || !endDate || !outputPath) {
  console.error("Error: startDate, endDate, and outputPath are required");
  console.error("Usage: node script.js 2024-03-15 2024-03-22 session/events.json");
  process.exit(1);
}

// Parse dates - accept YYYY-MM-DD or ISO8601
const parseDate = (dateStr, isEnd = false) => {
  // If already ISO8601 with time, use as-is
  if (dateStr.includes("T")) {
    return new Date(dateStr);
  }
  // Otherwise, treat as date in user's local context
  // For end date, use end of day
  const d = new Date(dateStr + "T00:00:00");
  if (isEnd) {
    d.setHours(23, 59, 59, 999);
  }
  return d;
};

const start = parseDate(startDate);
const end = parseDate(endDate, true);

console.log(`Fetching events from ${startDate} to ${endDate}...`);

try {
  // Get user's timezone first
  const calRes = await fetch(
    "https://www.googleapis.com/calendar/v3/calendars/primary",
    { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
  );
  const calData = await calRes.json();
  if (calData.error) throw new Error(`Calendar info failed: ${calData.error.message}`);

  const userTimezone = calData.timeZone;
  const userEmail = calData.id;

  console.log(`  User timezone: ${userTimezone}`);

  // Fetch events
  const events = [];
  let pageToken = null;

  do {
    const params = new URLSearchParams({
      timeMin: start.toISOString(),
      timeMax: end.toISOString(),
      singleEvents: "true",
      orderBy: "startTime",
      maxResults: "100",
    });
    if (pageToken) params.set("pageToken", pageToken);

    const res = await fetch(
      `https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
      { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
    );
    const data = await res.json();

    if (data.error) throw new Error(`Events fetch failed: ${data.error.message}`);

    events.push(...(data.items || []));
    pageToken = data.nextPageToken;
  } while (pageToken);

  console.log(`  Found ${events.length} events`);

  // Process events for readable output
  const processedEvents = events
    .filter((e) => e.status !== "cancelled")
    .map((event) => {
      const isAllDay = !event.start?.dateTime;
      const startTime = event.start?.dateTime || event.start?.date;
      const endTime = event.end?.dateTime || event.end?.date;

      // Calculate duration in minutes (only for timed events)
      let durationMinutes = null;
      if (!isAllDay && startTime && endTime) {
        durationMinutes = Math.round(
          (new Date(endTime) - new Date(startTime)) / (1000 * 60)
        );
      }

      // Process attendees (exclude self)
      const attendees = (event.attendees || [])
        .filter((a) => !a.self)
        .map((a) => ({
          name: a.displayName || a.email.split("@")[0],
          email: a.email,
          status: a.responseStatus,
        }));

      // Identify organizer
      const organizer = event.organizer
        ? {
            name: event.organizer.displayName || event.organizer.email?.split("@")[0],
            email: event.organizer.email,
            self: event.organizer.self || false,
          }
        : null;

      // Check for video conferencing
      const videoLink = event.conferenceData?.entryPoints?.find(
        (e) => e.entryPointType === "video"
      )?.uri || event.hangoutLink || null;

      return {
        id: event.id,
        title: event.summary || "(No title)",
        start: startTime,
        end: endTime,
        isAllDay,
        durationMinutes,
        timezone: event.start?.timeZone || userTimezone,
        location: event.location || null,
        videoLink,
        organizer,
        attendees,
        attendeeCount: attendees.length,
        isRecurring: !!event.recurringEventId,
        description: event.description || null,
      };
    });

  // Sort by start time (earliest first for query results)
  processedEvents.sort((a, b) => new Date(a.start) - new Date(b.start));

  // Group by date for easier display
  const eventsByDate = {};
  for (const event of processedEvents) {
    // Extract date part
    const dateKey = event.isAllDay
      ? event.start
      : event.start.split("T")[0];
    if (!eventsByDate[dateKey]) {
      eventsByDate[dateKey] = [];
    }
    eventsByDate[dateKey].push(event);
  }

  // Build output
  const output = {
    query: {
      start: startDate,
      end: endDate,
      timezone: userTimezone,
    },
    summary: {
      totalEvents: processedEvents.length,
      datesWithEvents: Object.keys(eventsByDate).length,
    },
    events: processedEvents,
    eventsByDate,
  };

  // Write output
  const dir = path.dirname(outputPath);
  if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\nāœ“ Found ${processedEvents.length} events`);
  console.log(`  Date range: ${startDate} to ${endDate}`);
  console.log(`  Output: ${outputPath}`);

  // Show preview
  if (processedEvents.length > 0) {
    console.log(`\n  Preview:`);
    processedEvents.slice(0, 5).forEach((e) => {
      const time = e.isAllDay ? "All day" : e.start.split("T")[1]?.slice(0, 5) || "";
      console.log(`    ${time} - ${e.title}${e.attendeeCount > 0 ? ` (${e.attendeeCount} attendees)` : ""}`);
    });
    if (processedEvents.length > 5) {
      console.log(`    ... and ${processedEvents.length - 5} more`);
    }
  }

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      eventCount: processedEvents.length,
      dateRange: { start: startDate, end: endDate },
    })
  );
} catch (error) {
  console.error("Failed:", error.message);
  throw error;
}